fix various issues
This commit is contained in:
@@ -32,7 +32,8 @@ export async function dispatchForMonitor(monitorId: string, event: NotificationE
|
||||
const channels = await sql<ChannelRow[]>`
|
||||
SELECT c.id, c.account_id, c.name, c.kind, c.config, c.enabled
|
||||
FROM notification_channels c
|
||||
WHERE c.id = ANY((SELECT channel_ids FROM monitors WHERE id = ${monitorId}))
|
||||
WHERE c.account_id = (SELECT account_id FROM monitors WHERE id = ${monitorId})
|
||||
AND ${monitorId} = ANY(c.monitor_ids)
|
||||
AND c.enabled = true
|
||||
`;
|
||||
if (channels.length === 0) return;
|
||||
|
||||
@@ -4,12 +4,23 @@ 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()),
|
||||
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()),
|
||||
monitor_ids: t.Optional(t.Array(t.String(), { description: "Monitor IDs this channel dispatches for." })),
|
||||
});
|
||||
|
||||
async function validateMonitorIds(accountId: string, monitorIds: string[]): Promise<string[]> {
|
||||
if (monitorIds.length === 0) return [];
|
||||
const owned = await sql<{ id: string }[]>`
|
||||
SELECT id FROM monitors
|
||||
WHERE account_id = ${accountId}
|
||||
AND id = ANY(${sql.array(monitorIds)}::text[])
|
||||
`;
|
||||
return owned.map((o) => o.id);
|
||||
}
|
||||
|
||||
function validateKind(kind: string): string | null {
|
||||
if (!knownProviderKinds().includes(kind)) {
|
||||
return `Unknown provider kind '${kind}'. Known: ${knownProviderKinds().join(", ")}`;
|
||||
@@ -35,7 +46,7 @@ export const channels = new Elysia({ prefix: "/notifications/channels" })
|
||||
|
||||
.get("/", async ({ accountId }) => {
|
||||
return sql`
|
||||
SELECT id, name, kind, config, enabled, created_at
|
||||
SELECT id, name, kind, config, enabled, monitor_ids, created_at
|
||||
FROM notification_channels
|
||||
WHERE account_id = ${accountId}
|
||||
ORDER BY created_at DESC
|
||||
@@ -48,10 +59,11 @@ export const channels = new Elysia({ prefix: "/notifications/channels" })
|
||||
const cfgErr = validateConfig(body.kind, body.config);
|
||||
if (cfgErr) { set.status = 400; return { error: cfgErr }; }
|
||||
|
||||
const monitorIds = body.monitor_ids ? await validateMonitorIds(accountId, body.monitor_ids) : [];
|
||||
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
|
||||
INSERT INTO notification_channels (account_id, name, kind, config, enabled, monitor_ids)
|
||||
VALUES (${accountId}, ${body.name}, ${body.kind}, ${sql.json(body.config)}, ${body.enabled ?? true}, ${sql.array(monitorIds)}::text[])
|
||||
RETURNING id, name, kind, config, enabled, monitor_ids, created_at
|
||||
`;
|
||||
return row;
|
||||
}, { body: ChannelBody, detail: { summary: "Create notification channel", tags: ["notifications"] } })
|
||||
@@ -69,14 +81,16 @@ export const channels = new Elysia({ prefix: "/notifications/channels" })
|
||||
}
|
||||
}
|
||||
|
||||
const validatedMonitorIds = body.monitor_ids ? await validateMonitorIds(accountId, body.monitor_ids) : null;
|
||||
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)
|
||||
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),
|
||||
monitor_ids = COALESCE(${validatedMonitorIds ? sql.array(validatedMonitorIds) : null}::text[], monitor_ids)
|
||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
RETURNING id, name, kind, config, enabled, created_at
|
||||
RETURNING id, name, kind, config, enabled, monitor_ids, created_at
|
||||
`;
|
||||
if (!row) { set.status = 404; return { error: "Not found" }; }
|
||||
return row;
|
||||
@@ -89,11 +103,6 @@ export const channels = new Elysia({ prefix: "/notifications/channels" })
|
||||
RETURNING id
|
||||
`;
|
||||
if (!row) { set.status = 404; return { error: "Not found" }; }
|
||||
// Remove this channel from any monitors that reference it.
|
||||
await sql`
|
||||
UPDATE monitors SET channel_ids = array_remove(channel_ids, ${params.id}::uuid)
|
||||
WHERE account_id = ${accountId} AND ${params.id}::uuid = ANY(channel_ids)
|
||||
`;
|
||||
return { deleted: true };
|
||||
}, { detail: { summary: "Delete notification channel", tags: ["notifications"] } })
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ const MonitorBody = t.Object({
|
||||
max_redirects: t.Optional(t.Number({ minimum: 0, maximum: 4, default: 1, description: "Follow up to N redirects. 0 = don't follow. Default 1." })),
|
||||
query: t.Optional(t.Any({ description: "PingQL query - filter conditions for up/down" })),
|
||||
regions: t.Optional(t.Array(t.String(), { description: "Regions to run checks from. Empty array = all regions." })),
|
||||
channel_ids: t.Optional(t.Array(t.String(), { description: "Notification channel IDs to attach to this monitor." })),
|
||||
tags: t.Optional(t.Array(t.String({ pattern: "^[a-z0-9][a-z0-9-]{0,40}$" }), { description: "Lowercase tag slugs for grouping. Replaces the existing tag set." })),
|
||||
});
|
||||
|
||||
@@ -28,16 +27,6 @@ function dedupeTags(tags: string[]): string[] {
|
||||
return Array.from(new Set(tags.map((t) => t.trim()).filter(Boolean)));
|
||||
}
|
||||
|
||||
async function validateChannelIds(accountId: string, channelIds: string[]): Promise<string[]> {
|
||||
if (channelIds.length === 0) return [];
|
||||
const owned = await sql<{ id: string }[]>`
|
||||
SELECT id FROM notification_channels
|
||||
WHERE account_id = ${accountId}
|
||||
AND id = ANY(${sql.array(channelIds)}::uuid[])
|
||||
`;
|
||||
return owned.map((o) => o.id);
|
||||
}
|
||||
|
||||
export const monitors = new Elysia({ prefix: "/monitors" })
|
||||
.use(requireAuth)
|
||||
|
||||
@@ -83,9 +72,8 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
||||
const ssrfError = await validateMonitorUrl(body.url);
|
||||
if (ssrfError) { set.status = 400; return { error: ssrfError }; }
|
||||
const tags = body.tags ? dedupeTags(body.tags) : [];
|
||||
const channelIds = body.channel_ids ? await validateChannelIds(accountId, body.channel_ids) : [];
|
||||
const [monitor] = await sql`
|
||||
INSERT INTO monitors (account_id, name, url, method, request_headers, request_body, timeout_ms, interval_s, max_retries, retry_interval_s, resend_interval, cert_alert_days, max_redirects, query, regions, tags, channel_ids)
|
||||
INSERT INTO monitors (account_id, name, url, method, request_headers, request_body, timeout_ms, interval_s, max_retries, retry_interval_s, resend_interval, cert_alert_days, max_redirects, query, regions, tags)
|
||||
VALUES (
|
||||
${accountId}, ${body.name}, ${body.url},
|
||||
${(body.method ?? 'GET').toUpperCase()},
|
||||
@@ -100,8 +88,7 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
||||
${body.max_redirects ?? 1},
|
||||
${body.query ? sql.json(body.query) : null},
|
||||
${sql.array(regions)},
|
||||
${sql.array(tags)},
|
||||
${sql.array(channelIds)}::uuid[]
|
||||
${sql.array(tags)}
|
||||
)
|
||||
RETURNING *
|
||||
`;
|
||||
@@ -145,7 +132,6 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
||||
if (ssrfError) { set.status = 400; return { error: ssrfError }; }
|
||||
}
|
||||
|
||||
const validatedChannelIds = body.channel_ids ? await validateChannelIds(accountId, body.channel_ids) : null;
|
||||
const [monitor] = await sql`
|
||||
UPDATE monitors SET
|
||||
name = COALESCE(${body.name ?? null}, name),
|
||||
@@ -163,7 +149,6 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
||||
query = COALESCE(${body.query ? sql.json(body.query) : null}, query),
|
||||
regions = COALESCE(${body.regions ? sql.array(body.regions) : null}, regions),
|
||||
tags = COALESCE(${body.tags ? sql.array(dedupeTags(body.tags)) : null}, tags),
|
||||
channel_ids = COALESCE(${validatedChannelIds ? sql.array(validatedChannelIds) : null}::uuid[], channel_ids),
|
||||
updated_at = now()
|
||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
RETURNING *
|
||||
@@ -178,6 +163,10 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
||||
DELETE FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id
|
||||
`;
|
||||
if (!deleted) { set.status = 404; return { error: "Not found" }; }
|
||||
await sql`
|
||||
UPDATE notification_channels SET monitor_ids = array_remove(monitor_ids, ${params.id})
|
||||
WHERE account_id = ${accountId} AND ${params.id} = ANY(monitor_ids)
|
||||
`;
|
||||
invalidateMonitorList();
|
||||
return { deleted: true };
|
||||
}, { detail: { summary: "Delete monitor", tags: ["monitors"] } })
|
||||
|
||||
Reference in New Issue
Block a user