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.
6.4 kB ยท 182 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183import { and, eq, inArray, isNull, ne, sql } from "@openstatus/db";import { page, pageComponent, pageSubscriber, pageSubscriberToPageComponent, selectPageSubscriberSchema,} from "@openstatus/db/src/schema";import { assertSafeUrl } from "@openstatus/utils";
import { emitAudit } from "../audit";import { requireScope } from "../auth";import { type ServiceContext, withTransaction } from "../context";import { ConflictError, NotFoundError, ValidationError } from "../errors";import { UpdatePageSubscriberChannelInput } from "./schemas";
/** * Update a vendor-added subscription's name / webhook URL / headers / * component scope. Page ownership is enforced by joining the subscriber * fetch to `page` โ a single SELECT that confirms (subscriber, page, * workspace) all line up. * * The auth check, channel write, and audit emit run in one transaction: * a failed audit insert rolls the channel write back, and there's no * race window between ownership confirmation and mutation. * * Row-level eligibility (vendor vs self-signup, email vs webhook) is * enforced inline below โ the legacy `updateChannel` helper in * `@openstatus/subscriptions` opened its own DB handle so it couldn't * share the caller's tx; the logic now lives here instead. */export async function updatePageSubscriberChannel(args: { ctx: ServiceContext; input: UpdatePageSubscriberChannelInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageSubscriberChannelInput.parse(args.input);
// String-only check (no DNS resolution) โ a public name pointing at a // private address still passes. Delivery and test sends never follow redirects. if (input.webhookUrl !== undefined) { await assertSafeUrl(input.webhookUrl); }
await withTransaction(ctx, async (tx) => { // One query confirms the subscriber exists, that it's attached to the // supplied page, and that the page belongs to the caller's workspace. // Any miss surfaces as 404 โ we don't distinguish "wrong page" from // "wrong workspace" to avoid leaking cross-workspace existence. const beforeJoined = await tx .select({ subscriber: pageSubscriber }) .from(pageSubscriber) .innerJoin(page, eq(pageSubscriber.pageId, page.id)) .where( and( eq(pageSubscriber.id, input.subscriberId), eq(pageSubscriber.pageId, input.pageId), eq(page.workspaceId, ctx.workspace.id), ), ) .get();
if (!beforeJoined) { throw new NotFoundError("page_subscriber", input.subscriberId); }
const existing = beforeJoined.subscriber;
if (existing.source !== "vendor") { throw new ValidationError( "Self-signup subscribers manage their own subscription; use the unsubscribe action instead.", ); }
if (existing.channelType === "email") { if (input.webhookUrl !== undefined || input.headers !== undefined) { throw new ValidationError( "Email subscribers do not have webhook fields to edit.", ); } }
const updateFields: Record<string, unknown> = { updatedAt: new Date() }; if (input.name !== undefined) updateFields.name = input.name;
if (existing.channelType === "webhook") { if ( input.webhookUrl !== undefined && input.webhookUrl !== existing.webhookUrl ) { const duplicate = await tx.query.pageSubscriber.findFirst({ where: and( eq(pageSubscriber.pageId, input.pageId), sql`LOWER(${pageSubscriber.webhookUrl}) = ${input.webhookUrl.toLowerCase()}`, eq(pageSubscriber.channelType, "webhook"), isNull(pageSubscriber.unsubscribedAt), ne(pageSubscriber.id, input.subscriberId), ), }); if (duplicate) { throw new ConflictError( "A subscriber with this webhook URL already exists for this page.", ); } updateFields.webhookUrl = input.webhookUrl; } if (input.headers !== undefined) { // Empty array is truthy in JS, so check `length > 0` โ otherwise // we'd persist `{"headers":[]}` instead of nulling the column. updateFields.channelConfig = input.headers.length > 0 ? JSON.stringify({ headers: input.headers }) : null; } }
if (input.componentIds !== undefined && input.componentIds.length > 0) { const valid = await tx .select({ id: pageComponent.id }) .from(pageComponent) .where( and( eq(pageComponent.pageId, input.pageId), inArray(pageComponent.id, input.componentIds), ), ) .all(); if (valid.length !== input.componentIds.length) { throw new ValidationError("Some components do not belong to this page"); } }
const updated = await tx .update(pageSubscriber) .set(updateFields) .where(eq(pageSubscriber.id, input.subscriberId)) .returning() .get();
if (input.componentIds !== undefined) { await tx .delete(pageSubscriberToPageComponent) .where( eq( pageSubscriberToPageComponent.pageSubscriberId, input.subscriberId, ), ) .run();
if (input.componentIds.length > 0) { await tx .insert(pageSubscriberToPageComponent) .values( input.componentIds.map((compId) => ({ pageSubscriberId: input.subscriberId, pageComponentId: compId, })), ) .run(); } }
// libSQL HTTP can omit RETURNING rows from a no-op UPDATE; ownership // was already proven via `beforeJoined`, so fall back to the pre-image. const before = selectPageSubscriberSchema.parse(existing); const after = selectPageSubscriberSchema.parse(updated ?? existing);
// `token` is the self-manage / unsubscribe capability; never audit it. const { token: _beforeToken, ...beforeSnap } = before; const { token: _afterToken, ...afterSnap } = after;
await emitAudit(tx, ctx, { action: "page_subscriber.update", entityType: "page_subscriber", entityId: input.subscriberId, before: beforeSnap, after: afterSnap, }); });}