- 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()
108 lines
3.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|