wulf-pulse/app/api/admin/notify-event-keys/route.ts
lorentz 7f4ffa0fb6 feat(09-06): add /admin/workflow/event-keys CRUD page and API routes
- GET/POST /api/admin/notify-event-keys: list ordered by sort_order/key, create with key regex validation (^[a-z][a-z0-9_]*$/i), 409 on conflict
- PUT/DELETE /api/admin/notify-event-keys/[key]: update via COALESCE, hard delete with 404 guard
- app/admin/workflow/event-keys/page.tsx: list with inline edit, Switch for is_active toggle, + New event key form, sonner toasts
- All routes gated by requireAdmin()
2026-05-10 07:41:27 -04:00

108 lines
3.6 KiB
TypeScript

/**
* Admin API — notify_event_keys list and create.
* GET: list all event keys ordered by sort_order, key.
* POST: create a new event key with key regex validation.
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/auth-utils';
import { postgresClient } from '@/lib/services/postgres-client';
const KEY_REGEX = /^[a-z][a-z0-9_]*$/i;
function toRow(row: Record<string, unknown>) {
return {
key: row.key,
displayLabel: row.display_label,
description: row.description,
sortOrder: row.sort_order,
isActive: row.is_active,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
export async function GET() {
const { error } = await requireAdmin();
if (error) return error;
try {
const result = await postgresClient.query(
`SELECT key, display_label, description, sort_order, is_active, created_at, updated_at
FROM notify_event_keys
ORDER BY sort_order ASC, key ASC`
);
return NextResponse.json({ data: result.rows.map(toRow) });
} catch (err) {
console.error('GET /api/admin/notify-event-keys failed:', err);
return NextResponse.json(
{ error: 'Failed to fetch event keys', message: err instanceof Error ? err.message : 'unknown' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
const { error } = await requireAdmin();
if (error) return error;
try {
const body = await request.json();
const { key, display_label, description, sort_order, is_active } = body;
if (!key || typeof key !== 'string' || key.length > 128) {
return NextResponse.json(
{ error: 'key must be a non-empty string of at most 128 characters' },
{ status: 400 }
);
}
if (!KEY_REGEX.test(key)) {
return NextResponse.json(
{ error: 'key must match /^[a-z][a-z0-9_]*$/i' },
{ status: 400 }
);
}
if (!display_label || typeof display_label !== 'string' || display_label.length > 200) {
return NextResponse.json(
{ error: 'display_label must be a non-empty string of at most 200 characters' },
{ status: 400 }
);
}
// INSERT with ON CONFLICT DO NOTHING; if key already exists return 409
await postgresClient.query(
`INSERT INTO notify_event_keys (key, display_label, description, sort_order, is_active)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (key) DO NOTHING`,
[key, display_label, description ?? null, sort_order ?? 0, is_active ?? true]
);
const result = await postgresClient.query(
`SELECT key, display_label, description, sort_order, is_active, created_at, updated_at
FROM notify_event_keys WHERE key = $1`,
[key]
);
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Conflict — key already exists' }, { status: 409 });
}
// Determine if we just inserted or it already existed
// If the created_at is very recent (within 1 second) we inserted it
const row = result.rows[0];
const createdAt = new Date(row.created_at as string).getTime();
const now = Date.now();
if (now - createdAt > 2000) {
// Row existed before this request
return NextResponse.json({ error: 'Conflict — key already exists' }, { status: 409 });
}
return NextResponse.json({ data: toRow(row) }, { status: 201 });
} catch (err) {
console.error('POST /api/admin/notify-event-keys failed:', err);
return NextResponse.json(
{ error: 'Failed to create event key', message: err instanceof Error ? err.message : 'unknown' },
{ status: 500 }
);
}
}