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.
9.9 kB ยท 373 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374import { eq } from "@openstatus/db";import { page } from "@openstatus/db/src/schema";
import { emitAudit } from "../audit";import { requireScope } from "../auth";import { type ServiceContext, withTransaction } from "../context";import { LimitExceededError } from "../errors";import { assertAccessTypeAllowed, assertSlugAvailable, getPageInWorkspace,} from "./internal";import { UpdatePageAppearanceInput, UpdatePageConfigurationInput, UpdatePageCustomDomainInput, UpdatePageCustomThemeInput, UpdatePageGeneralInput, UpdatePageLinksInput, UpdatePageLocalesInput, UpdatePagePasswordProtectionInput,} from "./schemas";
export async function updatePageGeneral(args: { ctx: ServiceContext; input: UpdatePageGeneralInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageGeneralInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
if (input.slug !== existing.slug) { await assertSlugAvailable({ tx, slug: input.slug, excludePageId: existing.id, }); }
const updated = await tx .update(page) .set({ title: input.title, slug: input.slug, description: input.description ?? "", icon: input.icon ?? "", updatedAt: new Date(), }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}
export async function updatePageCustomDomain(args: { ctx: ServiceContext; input: UpdatePageCustomDomainInput;}): Promise<{ existingDomain: string | null }> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageCustomDomainInput.parse(args.input);
if (input.customDomain && !ctx.workspace.limits["custom-domain"]) { throw new LimitExceededError("custom-domain", 0); }
return withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const updated = await tx .update(page) .set({ customDomain: input.customDomain, updatedAt: new Date() }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, });
return { existingDomain: existing.customDomain }; });}
/** * Update the page's access-type + all the fields scoped to it * (password / authEmailDomains / allowedIpRanges / allowIndex). * * The legacy boolean `passwordProtected` column isn't touched here โ * it's schema-deprecated and the v1 REST read path derives it from * `accessType` via `normalizePasswordProtected` (see * `apps/server/src/routes/v1/pages/schema.ts`). Deliberately omitted * so we're not writing two sources of truth for the same signal. */export async function updatePagePasswordProtection(args: { ctx: ServiceContext; input: UpdatePagePasswordProtectionInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePagePasswordProtectionInput.parse(args.input);
assertAccessTypeAllowed(ctx.workspace, { accessType: input.accessType, allowedIpRanges: input.allowedIpRanges ?? null, allowIndex: input.allowIndex, });
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const updated = await tx .update(page) .set({ accessType: input.accessType, // `|| null` (not `??`) on both array columns so three inputs // all clear the DB value: `undefined`, `null`, and `[]`. An // empty array joins to `""`, which `??` wouldn't coerce โ we'd // persist the empty string instead of nulling the column, // leaving a misleading "present but blank" state. `|| null` // treats `""` as falsy and maps it to `null`, while real // non-empty joins (e.g. `"a@b.com"` / `"10.0.0.0/24"`) pass // through unchanged. authEmailDomains: input.authEmailDomains?.join(",") || null, password: input.password, allowedIpRanges: input.allowedIpRanges?.join(",") || null, ...(input.allowIndex !== undefined && { allowIndex: input.allowIndex }), updatedAt: new Date(), }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}
export async function updatePageAppearance(args: { ctx: ServiceContext; input: UpdatePageAppearanceInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageAppearanceInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const currentConfiguration = typeof existing.configuration === "object" && existing.configuration !== null ? (existing.configuration as Record<string, unknown>) : {};
const updated = await tx .update(page) .set({ forceTheme: input.forceTheme, configuration: { ...currentConfiguration, theme: input.configuration.theme, }, updatedAt: new Date(), }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}
export async function updatePageCustomTheme(args: { ctx: ServiceContext; input: UpdatePageCustomThemeInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageCustomThemeInput.parse(args.input);
if (!ctx.workspace.limits["custom-theme"]) { throw new LimitExceededError("custom-theme", 0); }
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const updated = await tx .update(page) .set({ customTheme: input.customTheme, updatedAt: new Date() }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}
export async function updatePageLinks(args: { ctx: ServiceContext; input: UpdatePageLinksInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageLinksInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const updated = await tx .update(page) .set({ homepageUrl: input.homepageUrl, contactUrl: input.contactUrl, updatedAt: new Date(), }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}
export async function updatePageLocales(args: { ctx: ServiceContext; input: UpdatePageLocalesInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageLocalesInput.parse(args.input);
if (!ctx.workspace.limits.i18n) { throw new LimitExceededError("i18n", 0); }
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const updated = await tx .update(page) .set({ defaultLocale: input.defaultLocale, locales: input.locales, updatedAt: new Date(), }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}
export async function updatePageConfiguration(args: { ctx: ServiceContext; input: UpdatePageConfigurationInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdatePageConfigurationInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const existing = await getPageInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const currentConfiguration = typeof existing.configuration === "object" && existing.configuration !== null ? (existing.configuration as Record<string, unknown>) : {};
const updated = await tx .update(page) .set({ configuration: { ...currentConfiguration, ...Object.fromEntries( Object.entries(input.configuration ?? {}).filter( ([, value]) => value !== undefined, ), ), }, updatedAt: new Date(), }) .where(eq(page.id, existing.id)) .returning() .get();
await emitAudit(tx, ctx, { action: "page.update", entityType: "page", entityId: existing.id, before: existing, after: updated, }); });}