Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
12 kB ยท 382 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383import { Events } from "@openstatus/analytics";import { discordDataSchema, googleChatDataSchema, grafanaOncallDataSchema, msTeamsDataSchema, notificationProvider, ntfyDataSchema, opsgenieDataSchema, pagerdutyDataSchema, slackDataSchema, telegramDataSchema, webhookDataSchema, whatsappDataSchema,} from "@openstatus/db/src/schema";import { SchemaError } from "@openstatus/error";import { sendTest as sendWhatsAppTest } from "@openstatus/notification-bird-whatsapp";import { sendTestDiscordMessage as sendDiscordTest } from "@openstatus/notification-discord";import { sendTest as sendGoogleChatTest } from "@openstatus/notification-google-chat";import { sendTest as sendGrafanaTest } from "@openstatus/notification-grafana-oncall";import { sendTest as sendMsTeamsTest } from "@openstatus/notification-ms-teams";import { sendTest as sendNtfyTest } from "@openstatus/notification-ntfy";import { sendTest as sendOpsGenieTest } from "@openstatus/notification-opsgenie";import { PagerDutySchema, sendTest as sendPagerDutyTest,} from "@openstatus/notification-pagerduty";import { sendTestSlackMessage as sendSlackTest } from "@openstatus/notification-slack";import { sendTest as sendTelegramTest } from "@openstatus/notification-telegram";import { sendTest as sendWebhookTest } from "@openstatus/notification-webhook";import { NotFoundError } from "@openstatus/services";import { NotificationDataInputSchema, createNotification, deleteNotification, listNotifications, updateNotification,} from "@openstatus/services/notification";import { redis } from "@openstatus/upstash";import { TRPCError } from "@trpc/server";import { nanoid } from "nanoid";import { z } from "zod";
import { toServiceCtx, toTRPCError } from "../service-adapter";import { type TelegramGetUpdatesResponse, processTelegramUpdates,} from "../service/telegram-updates";import { createTRPCRouter, protectedProcedure } from "../trpc";
export const notificationRouter = createTRPCRouter({ list: protectedProcedure.query(async ({ ctx }) => { try { const { items } = await listNotifications({ ctx: toServiceCtx(ctx), input: { // Dashboard has no paging UI; match the sentinel pattern used // across the other domains. limit: 10_000, offset: 0, order: "desc", }, }); return items; } catch (err) { toTRPCError(err); } }),
// TODO: rename to update after migration updateNotifier: protectedProcedure .meta({ track: Events.UpdateNotification }) .input( z.object({ id: z.number(), name: z.string(), data: NotificationDataInputSchema, monitors: z.array(z.number()), }), ) .mutation(async ({ ctx, input }) => { try { await updateNotification({ ctx: toServiceCtx(ctx), input: { id: input.id, name: input.name, data: input.data, monitors: input.monitors, }, }); } catch (err) { toTRPCError(err); } }),
new: protectedProcedure .meta({ track: Events.CreateNotification, trackProps: ["provider"] }) .input( z.object({ provider: z.enum(notificationProvider), data: NotificationDataInputSchema, name: z.string(), monitors: z.array(z.number()).prefault([]), }), ) .mutation(async ({ ctx, input }) => { try { return await createNotification({ ctx: toServiceCtx(ctx), input: { name: input.name, provider: input.provider, data: input.data, monitors: input.monitors, }, }); } catch (err) { toTRPCError(err); } }),
delete: protectedProcedure .meta({ track: Events.DeleteNotification }) .input(z.object({ id: z.number() })) .mutation(async ({ ctx, input }) => { try { await deleteNotification({ ctx: toServiceCtx(ctx), input: { id: input.id }, }); } catch (err) { // Preserve the pre-migration idempotent behaviour โ the old tRPC // delete didn't throw on missing. if (err instanceof NotFoundError) return; toTRPCError(err); } }),
sendTest: protectedProcedure .input( z.object({ provider: z.enum(notificationProvider), data: NotificationDataInputSchema, }), ) .mutation(async (opts) => { if (opts.input.provider === "telegram") { const _data = telegramDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); } await sendTelegramTest({ chatId: _data.data.telegram.chatId, });
return; } if (opts.input.provider === "whatsapp") { const _data = whatsappDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); } await sendWhatsAppTest({ phoneNumber: _data.data.whatsapp });
return; } if (opts.input.provider === "google-chat") { const _data = googleChatDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
await sendGoogleChatTest(_data.data["google-chat"]); return; } if (opts.input.provider === "grafana-oncall") { const _data = grafanaOncallDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
if (!(await sendGrafanaTest(_data.data["grafana-oncall"]))) { throw new TRPCError({ code: "BAD_REQUEST", message: "Failed to send test", }); } return; } if (opts.input.provider === "ms-teams") { const _data = msTeamsDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
await sendMsTeamsTest({ webhookUrl: _data.data["ms-teams"].webhookUrl, }); return; } if (opts.input.provider === "discord") { const _data = discordDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
await sendDiscordTest(_data.data.discord); return; } if (opts.input.provider === "slack") { const _data = slackDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
await sendSlackTest(_data.data.slack); return; } if (opts.input.provider === "webhook") { const _data = webhookDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
await sendWebhookTest({ url: _data.data.webhook.endpoint, headers: _data.data.webhook.headers, }); return; } if (opts.input.provider === "opsgenie") { const _data = opsgenieDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
await sendOpsGenieTest(_data.data.opsgenie); return; } if (opts.input.provider === "ntfy") { const _data = ntfyDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
if (!(await sendNtfyTest(_data.data.ntfy))) { throw new TRPCError({ code: "BAD_REQUEST", message: "Failed to send test", }); } return; } if (opts.input.provider === "pagerduty") { const _data = pagerdutyDataSchema.safeParse(opts.input.data); if (!_data.success) { throw new TRPCError({ code: "BAD_REQUEST", message: SchemaError.fromZod(_data.error, opts.input).message, }); }
let rawPagerduty: unknown; try { rawPagerduty = JSON.parse(_data.data.pagerduty); } catch { throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid PagerDuty configuration: malformed JSON", }); }
const parsed = PagerDutySchema.safeParse(rawPagerduty); if (!parsed.success) { throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid PagerDuty configuration", }); }
if (parsed.data.integration_keys.length === 0) { throw new TRPCError({ code: "BAD_REQUEST", message: "No PagerDuty integration key provided", }); }
await sendPagerDutyTest({ integrationKey: parsed.data.integration_keys[0].integration_key, }); return; }
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid provider", }); }),
createTelegramToken: protectedProcedure.query(async (opts) => { const workspaceId = opts.ctx.workspace.id; const randomId = nanoid(12); const EXPIRY = 1800; // 30 minutes
try { await redis.set(`telegram:workspace_token:${workspaceId}`, randomId, { ex: EXPIRY, }); return { token: randomId, redisAvailable: true }; } catch (error) { // Redis unavailable (e.g., self-hosted without Redis) // Return null token to signal frontend to use manual setup only console.warn("Redis unavailable for Telegram token storage:", error); return { token: null, redisAvailable: false }; } }),
getTelegramUpdates: protectedProcedure .input( z .object({ privateChatId: z.string().optional(), since: z.number().optional(), }) .optional(), ) .query(async (opts) => { const res = await fetch( `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/getUpdates`, ); const data = (await res.json()) as TelegramGetUpdatesResponse; if (!data.ok || !data.result) return [];
const botUsername = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME; if (!botUsername) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Telegram bot username not configured", }); }
return processTelegramUpdates({ updates: data.result, workspaceId: opts.ctx.workspace.id, privateChatId: opts.input?.privateChatId, since: opts.input?.since, botUsername, redisClient: redis, }); }),});