113 lines
4.5 KiB
TypeScript
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"] } });
|