import { Elysia, t } from "elysia"; import { requireAuth } from "./auth"; import sql from "../db"; import { dispatch, knownProviderKinds, type ChannelRow } from "../notifications"; const ChannelBody = t.Object({ name: t.String({ minLength: 1, maxLength: 200 }), kind: t.String({ description: "Provider kind, e.g. 'webhook'" }), config: t.Any({ description: "Provider-specific config object" }), enabled: t.Optional(t.Boolean()), }); function validateKind(kind: string): string | null { if (!knownProviderKinds().includes(kind)) { return `Unknown provider kind '${kind}'. Known: ${knownProviderKinds().join(", ")}`; } return null; } function validateWebhookConfig(config: any): string | null { if (!config || typeof config !== "object") return "config must be an object"; if (typeof config.url !== "string" || !/^https?:\/\//.test(config.url)) { return "webhook config.url must be a http(s) URL"; } return null; } function validateConfig(kind: string, config: any): string | null { if (kind === "webhook") return validateWebhookConfig(config); return null; } export const channels = new Elysia({ prefix: "/notifications/channels" }) .use(requireAuth) .get("/", async ({ accountId }) => { return sql` SELECT id, name, kind, config, enabled, created_at FROM notification_channels WHERE account_id = ${accountId} ORDER BY created_at DESC `; }, { detail: { summary: "List notification channels", tags: ["notifications"] } }) .post("/", async ({ accountId, body, set }) => { const kindErr = validateKind(body.kind); if (kindErr) { set.status = 400; return { error: kindErr }; } const cfgErr = validateConfig(body.kind, body.config); if (cfgErr) { set.status = 400; return { error: cfgErr }; } const [row] = await sql` INSERT INTO notification_channels (account_id, name, kind, config, enabled) VALUES (${accountId}, ${body.name}, ${body.kind}, ${sql.json(body.config)}, ${body.enabled ?? true}) RETURNING id, name, kind, config, enabled, created_at `; return row; }, { body: ChannelBody, detail: { summary: "Create notification channel", tags: ["notifications"] } }) .patch("/:id", async ({ accountId, params, body, set }) => { if (body.kind != null) { const kindErr = validateKind(body.kind); if (kindErr) { set.status = 400; return { error: kindErr }; } } if (body.config != null) { const kind = body.kind ?? (await sql`SELECT kind FROM notification_channels WHERE id = ${params.id} AND account_id = ${accountId}`)[0]?.kind; if (kind) { const cfgErr = validateConfig(kind, body.config); if (cfgErr) { set.status = 400; return { error: cfgErr }; } } } const [row] = await sql` UPDATE notification_channels SET name = COALESCE(${body.name ?? null}, name), kind = COALESCE(${body.kind ?? null}, kind), config = COALESCE(${body.config != null ? sql.json(body.config) : null}, config), enabled = COALESCE(${body.enabled ?? null}, enabled) WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id, name, kind, config, enabled, created_at `; if (!row) { set.status = 404; return { error: "Not found" }; } return row; }, { body: t.Partial(ChannelBody), detail: { summary: "Update notification channel", tags: ["notifications"] } }) .delete("/:id", async ({ accountId, params, set }) => { const [row] = await sql` DELETE FROM notification_channels WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id `; if (!row) { set.status = 404; return { error: "Not found" }; } return { deleted: true }; }, { detail: { summary: "Delete notification channel", tags: ["notifications"] } }) .post("/:id/test", async ({ accountId, params, set }) => { const [row] = await sql` SELECT id, account_id, name, kind, config, enabled FROM notification_channels WHERE id = ${params.id} AND account_id = ${accountId} `; if (!row) { set.status = 404; return { error: "Not found" }; } try { await dispatch(row, { kind: "test", monitor: { id: "test", name: "Test event", url: "https://example.com", region: "" }, }); return { ok: true }; } catch (e: any) { set.status = 502; return { error: e?.message || String(e) }; } }, { detail: { summary: "Send a test event to a channel", tags: ["notifications"] } });