mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 09:54:26 +00:00
fix(webhooks): hide write affordances from non-admin paid users (#1176)
* fix(webhooks): hide write affordances from non-admin paid users Backend gates POST/PUT/DELETE /api/webhooks with `requireAdmin`, but WebhooksSection rendered the Create webhook button, the New-webhook form, the per-row toggle, and the delete button for any paid user. A non-admin operator clicked through to a 403 error toast. WebhooksSection now reads `isAdmin` from AuthContext and unmounts those four affordances when the operator is not admin. Non-admins still see the list, trigger URLs, masked secrets, and execution history per the docs promise at docs/features/webhooks.mdx:7. A read-only On/Off chip stands in for the toggle so the enabled state stays visible. A useEffect resets `showForm` whenever `isAdmin` flips back to false, so the form cannot remain open across a role downgrade. * fix(webhooks): correct settings entry description for incoming webhooks The Webhooks entry in the settings registry described the sibling notification-routing feature: "Outbound HTTP hooks to Slack, Discord, Teams, or custom endpoints" with keywords for those channels. The actual page configures incoming HMAC-signed triggers that run stack actions from CI/CD pipelines. Update the description to match the real feature and replace the keywords so settings search resolves "trigger", "ci", "hmac", and "incoming" to the Webhooks page (and stops resolving "slack"/"discord" there).
This commit is contained in:
@@ -5,6 +5,7 @@ import { TogglePill } from '@/components/ui/toggle-pill';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { toast } from '@/components/ui/toast-store';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useNodes } from '@/context/NodeContext';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { copyToClipboard } from '@/lib/clipboard';
|
||||
@@ -42,6 +43,7 @@ interface WebhookExecution {
|
||||
}
|
||||
|
||||
export function WebhooksSection({ isPaid }: { isPaid: boolean }) {
|
||||
const { isAdmin } = useAuth();
|
||||
const { activeNode, nodes } = useNodes();
|
||||
const [webhooks, setWebhooks] = useState<WebhookItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -72,6 +74,7 @@ export function WebhooksSection({ isPaid }: { isPaid: boolean }) {
|
||||
};
|
||||
|
||||
useEffect(() => { fetchWebhooks(); fetchStacks(); }, [activeNode?.id]);
|
||||
useEffect(() => { if (!isAdmin) setShowForm(false); }, [isAdmin]);
|
||||
|
||||
const enabledCount = webhooks.filter(w => w.enabled).length;
|
||||
useMastheadStats(
|
||||
@@ -159,13 +162,15 @@ export function WebhooksSection({ isPaid }: { isPaid: boolean }) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex justify-end">
|
||||
<SettingsPrimaryButton size="sm" onClick={() => setShowForm(!showForm)}>
|
||||
<Plus className="w-4 h-4" /> Create webhook
|
||||
</SettingsPrimaryButton>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="flex justify-end">
|
||||
<SettingsPrimaryButton size="sm" onClick={() => setShowForm(!showForm)}>
|
||||
<Plus className="w-4 h-4" /> Create webhook
|
||||
</SettingsPrimaryButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showForm && (
|
||||
{isAdmin && showForm && (
|
||||
<SettingsSection title="New webhook">
|
||||
<SettingsField label="Name" helper="Shown in execution history and notifications." htmlFor="webhook-name">
|
||||
<Input id="webhook-name" placeholder="Deploy on push" value={formName} onChange={e => setFormName(e.target.value)} />
|
||||
@@ -240,7 +245,9 @@ export function WebhooksSection({ isPaid }: { isPaid: boolean }) {
|
||||
<SettingsCallout
|
||||
icon={<Webhook className="h-4 w-4" />}
|
||||
title="No webhooks yet"
|
||||
subtitle="Create one to trigger stack actions from CI/CD."
|
||||
subtitle={isAdmin
|
||||
? 'Create one to trigger stack actions from CI/CD.'
|
||||
: 'An admin operator can create webhooks for this instance.'}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -269,10 +276,18 @@ export function WebhooksSection({ isPaid }: { isPaid: boolean }) {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<TogglePill checked={wh.enabled} onChange={(c) => handleToggle(wh.id!, c)} />
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => handleDelete(wh.id!)}>
|
||||
<Trash2 className="w-4 h-4 text-stat-subtitle" />
|
||||
</Button>
|
||||
{isAdmin ? (
|
||||
<>
|
||||
<TogglePill checked={wh.enabled} onChange={(c) => handleToggle(wh.id!, c)} />
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => handleDelete(wh.id!)}>
|
||||
<Trash2 className="w-4 h-4 text-stat-subtitle" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle border border-card-border rounded px-1.5 py-0.5">
|
||||
{wh.enabled ? 'On' : 'Off'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -159,8 +159,8 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [
|
||||
id: 'webhooks',
|
||||
group: 'alerts',
|
||||
label: 'Webhooks',
|
||||
description: 'Outbound HTTP hooks to Slack, Discord, Teams, or custom endpoints.',
|
||||
keywords: ['slack', 'discord', 'teams', 'webhook', 'outbound'],
|
||||
description: 'Incoming HMAC-signed HTTP triggers that run stack actions from CI/CD pipelines.',
|
||||
keywords: ['webhook', 'incoming', 'trigger', 'ci', 'cd', 'pipeline', 'deploy', 'hmac', 'signature', 'action'],
|
||||
tier: 'skipper',
|
||||
scope: 'global',
|
||||
hiddenOnRemote: true,
|
||||
|
||||
Reference in New Issue
Block a user