|
|
|
@@ -23,28 +23,18 @@ const MonitorBody = t.Object({
|
|
|
|
|
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." })),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function replaceMonitorTags(monitorId: string, tags: string[]) {
|
|
|
|
|
await sql`DELETE FROM monitor_tags WHERE monitor_id = ${monitorId}`;
|
|
|
|
|
if (tags.length === 0) return;
|
|
|
|
|
const unique = Array.from(new Set(tags.map((t) => t.trim()).filter(Boolean)));
|
|
|
|
|
if (unique.length === 0) return;
|
|
|
|
|
const rows = unique.map((tag) => ({ monitor_id: monitorId, tag }));
|
|
|
|
|
await sql`INSERT INTO monitor_tags ${sql(rows, "monitor_id", "tag")}`;
|
|
|
|
|
function dedupeTags(tags: string[]): string[] {
|
|
|
|
|
return Array.from(new Set(tags.map((t) => t.trim()).filter(Boolean)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function replaceMonitorChannels(monitorId: string, accountId: string, channelIds: string[]) {
|
|
|
|
|
await sql`DELETE FROM monitor_notifications WHERE monitor_id = ${monitorId}`;
|
|
|
|
|
if (channelIds.length === 0) return;
|
|
|
|
|
// Only attach channels that belong to the same account. Cast to uuid[] so the
|
|
|
|
|
// ANY() comparison against the uuid id column type-checks.
|
|
|
|
|
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[])
|
|
|
|
|
`;
|
|
|
|
|
if (owned.length === 0) return;
|
|
|
|
|
const rows = owned.map((o) => ({ monitor_id: monitorId, channel_id: o.id }));
|
|
|
|
|
await sql`INSERT INTO monitor_notifications ${sql(rows, "monitor_id", "channel_id")}`;
|
|
|
|
|
return owned.map((o) => o.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const monitors = new Elysia({ prefix: "/monitors" })
|
|
|
|
@@ -54,10 +44,9 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
|
|
|
|
const tag = (query as any)?.tag;
|
|
|
|
|
if (tag) {
|
|
|
|
|
return sql`
|
|
|
|
|
SELECT m.* FROM monitors m
|
|
|
|
|
JOIN monitor_tags mt ON mt.monitor_id = m.id
|
|
|
|
|
WHERE m.account_id = ${accountId} AND mt.tag = ${tag}
|
|
|
|
|
ORDER BY m.created_at DESC
|
|
|
|
|
SELECT * FROM monitors
|
|
|
|
|
WHERE account_id = ${accountId} AND ${tag} = ANY(tags)
|
|
|
|
|
ORDER BY created_at DESC
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
return sql`SELECT * FROM monitors WHERE account_id = ${accountId} ORDER BY created_at DESC`;
|
|
|
|
@@ -92,8 +81,10 @@ 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, query, regions)
|
|
|
|
|
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, query, regions, tags, channel_ids)
|
|
|
|
|
VALUES (
|
|
|
|
|
${accountId}, ${body.name}, ${body.url},
|
|
|
|
|
${(body.method ?? 'GET').toUpperCase()},
|
|
|
|
@@ -106,12 +97,12 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
|
|
|
|
${body.resend_interval ?? 0},
|
|
|
|
|
${body.cert_alert_days ?? 0},
|
|
|
|
|
${body.query ? sql.json(body.query) : null},
|
|
|
|
|
${sql.array(regions)}
|
|
|
|
|
${sql.array(regions)},
|
|
|
|
|
${sql.array(tags)},
|
|
|
|
|
${sql.array(channelIds)}::uuid[]
|
|
|
|
|
)
|
|
|
|
|
RETURNING *
|
|
|
|
|
`;
|
|
|
|
|
if (body.channel_ids) await replaceMonitorChannels(monitor.id, accountId, body.channel_ids);
|
|
|
|
|
if (body.tags) await replaceMonitorTags(monitor.id, body.tags);
|
|
|
|
|
invalidateMonitorList();
|
|
|
|
|
return monitor;
|
|
|
|
|
}, { body: MonitorBody, detail: { summary: "Create monitor", tags: ["monitors"] } })
|
|
|
|
@@ -126,13 +117,7 @@ export const monitors = new Elysia({ prefix: "/monitors" })
|
|
|
|
|
SELECT * FROM pings WHERE monitor_id = ${params.id}
|
|
|
|
|
ORDER BY checked_at DESC LIMIT 100
|
|
|
|
|
`;
|
|
|
|
|
const channels = await sql<{ channel_id: string }[]>`
|
|
|
|
|
SELECT channel_id FROM monitor_notifications WHERE monitor_id = ${params.id}
|
|
|
|
|
`;
|
|
|
|
|
const tagRows = await sql<{ tag: string }[]>`
|
|
|
|
|
SELECT tag FROM monitor_tags WHERE monitor_id = ${params.id} ORDER BY tag
|
|
|
|
|
`;
|
|
|
|
|
return { ...monitor, results, channel_ids: channels.map((c) => c.channel_id), tags: tagRows.map((t) => t.tag) };
|
|
|
|
|
return { ...monitor, results };
|
|
|
|
|
}, { detail: { summary: "Get monitor with results", tags: ["monitors"] } })
|
|
|
|
|
|
|
|
|
|
.patch("/:id", async ({ accountId, plan, params, body, set }) => {
|
|
|
|
@@ -158,27 +143,29 @@ 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),
|
|
|
|
|
url = COALESCE(${body.url ?? null}, url),
|
|
|
|
|
method = COALESCE(${body.method ? body.method.toUpperCase() : null}, method),
|
|
|
|
|
request_headers = COALESCE(${body.request_headers ? sql.json(body.request_headers) : null}, request_headers),
|
|
|
|
|
request_body = COALESCE(${body.request_body ?? null}, request_body),
|
|
|
|
|
timeout_ms = COALESCE(${body.timeout_ms ?? null}, timeout_ms),
|
|
|
|
|
interval_s = COALESCE(${body.interval_s ?? null}, interval_s),
|
|
|
|
|
max_retries = COALESCE(${body.max_retries ?? null}, max_retries),
|
|
|
|
|
name = COALESCE(${body.name ?? null}, name),
|
|
|
|
|
url = COALESCE(${body.url ?? null}, url),
|
|
|
|
|
method = COALESCE(${body.method ? body.method.toUpperCase() : null}, method),
|
|
|
|
|
request_headers = COALESCE(${body.request_headers ? sql.json(body.request_headers) : null}, request_headers),
|
|
|
|
|
request_body = COALESCE(${body.request_body ?? null}, request_body),
|
|
|
|
|
timeout_ms = COALESCE(${body.timeout_ms ?? null}, timeout_ms),
|
|
|
|
|
interval_s = COALESCE(${body.interval_s ?? null}, interval_s),
|
|
|
|
|
max_retries = COALESCE(${body.max_retries ?? null}, max_retries),
|
|
|
|
|
retry_interval_s = COALESCE(${body.retry_interval_s ?? null}, retry_interval_s),
|
|
|
|
|
resend_interval = COALESCE(${body.resend_interval ?? null}, resend_interval),
|
|
|
|
|
cert_alert_days = COALESCE(${body.cert_alert_days ?? null}, cert_alert_days),
|
|
|
|
|
query = COALESCE(${body.query ? sql.json(body.query) : null}, query),
|
|
|
|
|
regions = COALESCE(${body.regions ? sql.array(body.regions) : null}, regions)
|
|
|
|
|
resend_interval = COALESCE(${body.resend_interval ?? null}, resend_interval),
|
|
|
|
|
cert_alert_days = COALESCE(${body.cert_alert_days ?? null}, cert_alert_days),
|
|
|
|
|
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 *
|
|
|
|
|
`;
|
|
|
|
|
if (!monitor) { set.status = 404; return { error: "Not found" }; }
|
|
|
|
|
if (body.channel_ids) await replaceMonitorChannels(monitor.id, accountId, body.channel_ids);
|
|
|
|
|
if (body.tags) await replaceMonitorTags(monitor.id, body.tags);
|
|
|
|
|
invalidateMonitorList();
|
|
|
|
|
return monitor;
|
|
|
|
|
}, { body: t.Partial(MonitorBody), detail: { summary: "Update monitor", tags: ["monitors"] } })
|
|
|
|
|