wulf-pulse/app/api/notification-channels/route.ts
lorentz 47cab788fc 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
2026-05-10 07:39:53 -04:00

84 lines
3.1 KiB
TypeScript

/**
* Notification Channels API — list and create channels.
* GET: requires auth; admins see all rows (with owner_email); non-admins see global-only.
* POST: requires admin.
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin } from '@/lib/auth-utils';
import { postgresClient } from '@/lib/services/postgres-client';
export async function GET(request: NextRequest) {
const { session, error } = await requireAuth();
if (error) return error;
const userRole = (session!.user as { role?: string }).role ?? 'user';
const isAdmin = userRole === 'admin' || userRole === 'super-admin';
try {
if (isAdmin) {
const ownerFilter = request.nextUrl.searchParams.get('owner') ?? 'all';
let whereSql = '';
if (ownerFilter === 'global') {
whereSql = 'WHERE nc.owner_user_id IS NULL';
} else if (ownerFilter === 'personal') {
whereSql = 'WHERE nc.owner_user_id IS NOT NULL';
}
const result = await postgresClient.query(
`SELECT nc.*, u.email AS owner_email
FROM notification_channels nc
LEFT JOIN "user" u ON u.id = nc.owner_user_id
${whereSql}
ORDER BY nc.owner_user_id NULLS FIRST, nc.name`
);
return NextResponse.json({ data: result.rows, total: result.rows.length });
} else {
// Non-admins: only global channels
const result = await postgresClient.query(
`SELECT * FROM notification_channels
WHERE owner_user_id IS NULL
ORDER BY name`
);
return NextResponse.json({ data: result.rows, total: result.rows.length });
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error('GET /api/notification-channels failed:', err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const { error } = await requireAdmin();
if (error) return error;
try {
const body = await request.json();
const { name, channel_type, config, is_active, owner_user_id } = body;
if (!name || !channel_type) {
return NextResponse.json({ error: 'name and channel_type are required' }, { status: 400 });
}
// LOW 12 explicit acceptance: all four channel_type values remain accepted
const validTypes = ['teams', 'telegram', 'ntfy', 'webhook'];
if (!validTypes.includes(channel_type)) {
return NextResponse.json({ error: `channel_type must be one of: ${validTypes.join(', ')}` }, { status: 400 });
}
const result = await postgresClient.query(
`INSERT INTO notification_channels (name, channel_type, config, is_active, owner_user_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[name, channel_type, JSON.stringify(config || {}), is_active ?? true, owner_user_id ?? null]
);
return NextResponse.json(result.rows[0], { status: 201 });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error('POST /api/notification-channels failed:', err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}