From 47cab788fcf6ead2d3a1a735d5a8df92a49b2547 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 10 May 2026 07:39:53 -0400 Subject: [PATCH] feat(09-06): owner column + role-scoped reads on notification channels - GET /api/notification-channels: requireAuth(), admin sees all rows with owner_email JOIN, non-admin sees global-only - GET accepts ?owner=global|personal|all filter parameter - POST /api/notification-channels: requireAdmin(); preserves all four channel_type values (teams/telegram/ntfy/webhook); adds owner_user_id column - [id] routes: requireAuth() + per-row authorization (isAdmin || isOwner); global rows require admin - Admin channels page: Owner badge (Global vs Personal: email), Show filter select, disclaimer text for personal channels --- app/admin/workflow/channels/page.tsx | 63 +++++++++++++--- app/api/notification-channels/[id]/route.ts | 82 ++++++++++++++++++--- app/api/notification-channels/route.ts | 64 ++++++++++++---- 3 files changed, 174 insertions(+), 35 deletions(-) diff --git a/app/admin/workflow/channels/page.tsx b/app/admin/workflow/channels/page.tsx index 68b7624..d6cd9ba 100644 --- a/app/admin/workflow/channels/page.tsx +++ b/app/admin/workflow/channels/page.tsx @@ -6,6 +6,13 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { ArrowLeft, Plus, @@ -24,12 +31,16 @@ interface Channel { id: number; name: string; channel_type: string; - config: Record; + config: Record; is_active: boolean; + owner_user_id: string | null; + owner_email?: string | null; created_at: string; updated_at: string; } +type OwnerFilter = 'all' | 'global' | 'personal'; + const CHANNEL_TYPES = [ { value: 'teams', label: 'Microsoft Teams', icon: MessageSquare, color: 'bg-indigo-100 text-indigo-700', fields: [ { key: 'webhook_url', label: 'Webhook URL', type: 'url', placeholder: 'https://...webhook.office.com/...' }, @@ -54,19 +65,23 @@ const CHANNEL_TYPES = [ export default function ChannelsPage() { const [channels, setChannels] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [ownerFilter, setOwnerFilter] = useState('all'); const [showCreate, setShowCreate] = useState(false); const [editingId, setEditingId] = useState(null); const [formType, setFormType] = useState('teams'); const [formName, setFormName] = useState(''); - const [formConfig, setFormConfig] = useState>({}); + const [formConfig, setFormConfig] = useState>({}); const [testStatus, setTestStatus] = useState>({}); - useEffect(() => { loadChannels(); }, []); + useEffect(() => { loadChannels(); }, [ownerFilter]); // eslint-disable-line react-hooks/exhaustive-deps const loadChannels = async () => { setIsLoading(true); try { - const res = await fetch('/api/notification-channels'); + const url = ownerFilter === 'all' + ? '/api/notification-channels' + : `/api/notification-channels?owner=${ownerFilter}`; + const res = await fetch(url); if (res.ok) { const data = await res.json(); setChannels(data.data || []); @@ -176,6 +191,27 @@ export default function ChannelsPage() { + {/* Owner filter */} +
+ Show: + +
+ + {ownerFilter !== 'global' && ( +

+ Personal channels contain user-supplied webhook URLs — handle with care. +

+ )} + {showCreate && ( @@ -215,7 +251,7 @@ export default function ChannelsPage() { {field.type === 'select' ? (