pingql/apps/api/src/routes/channels.ts

113 lines
4.5 KiB
TypeScript

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<ChannelRow[]>`
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"] } });