feat: add custom domains

This commit is contained in:
2026-04-10 05:09:28 +04:00
parent 34470197d1
commit b80258f17e
9 changed files with 201 additions and 13 deletions
+11
View File
@@ -20,6 +20,7 @@ export interface StatusPageRow {
show_cert_expiry: boolean;
bar_frequency: BucketType;
bar_count: number;
custom_domain: string | null;
custom_css: string | null;
footer_text: string | null;
og_image_url: string | null;
@@ -86,6 +87,16 @@ export async function loadStatusPage(slug: string): Promise<StatusPageRow | null
return row ?? null;
}
export async function loadStatusPageByDomain(domain: string): Promise<StatusPageRow | null> {
const [row] = await sql<StatusPageRow[]>`SELECT * FROM status_pages WHERE custom_domain = ${domain}`;
return row ?? null;
}
export async function verifyDomain(domain: string): Promise<boolean> {
const [row] = await sql<{ id: string }[]>`SELECT id FROM status_pages WHERE custom_domain = ${domain}`;
return !!row;
}
export async function loadGroups(pageId: string): Promise<GroupRow[]> {
return sql<GroupRow[]>`
SELECT id, name, position FROM status_page_groups
+56 -2
View File
@@ -3,7 +3,7 @@ import { Eta } from "eta";
import { resolve } from "path";
import { createHash } from "crypto";
import sql from "./db";
import { loadStatusPage, loadPagePayload, loadMonitorDetail } from "./data";
import { loadStatusPage, loadStatusPageByDomain, loadPagePayload, loadMonitorDetail, verifyDomain } from "./data";
import { renderRss } from "./render/rss";
import { renderBadge, badgeFromState } from "./render/badge";
import { cached } from "./cache";
@@ -88,6 +88,19 @@ function isAuthorised(page: { id: string; password_hash: string | null }, req: R
// Memoirist (Elysia's router) treats `:slug.json` as a single parameter named
// "slug.json" and refuses to coexist with `:slug`. Instead, use one `:slug`
// route and dispatch on the suffix in the handler.
const STATUS_HOST = process.env.STATUS_HOST || "status.pingql.com";
function isCustomDomain(request: Request): string | null {
const host = new URL(request.url).hostname;
if (host === STATUS_HOST || host === "localhost" || host === "127.0.0.1") return null;
return host;
}
async function resolveSlugFromDomain(domain: string): Promise<string | null> {
const page = await loadStatusPageByDomain(domain);
return page?.slug ?? null;
}
function splitSlugAndFormat(raw: string): { slug: string; format: "html" | "json" | "rss" } {
if (raw.endsWith(".json")) return { slug: raw.slice(0, -5), format: "json" };
if (raw.endsWith(".rss")) return { slug: raw.slice(0, -4), format: "rss" };
@@ -183,7 +196,48 @@ async function renderRssResp(slug: string, request: Request): Promise<Response>
const app = new Elysia()
// No status page lives at the root - show the same 404 visitors get for any
// unknown slug, so a stray hit on the apex doesn't leak service identity.
.get("/", () => notFound())
// Caddy on_demand TLS verification - confirms a domain is a valid custom domain
.get("/internal/verify-domain", async ({ query }) => {
const domain = (query as any)?.domain;
if (!domain) return new Response("missing domain", { status: 400 });
const valid = await verifyDomain(domain);
return new Response(valid ? "ok" : "not found", { status: valid ? 200 : 404 });
})
// Custom domain routes - serve at root when Host is a custom domain
.get("/", async ({ request }) => {
const domain = isCustomDomain(request);
if (!domain) return notFound();
const slug = await resolveSlugFromDomain(domain);
if (!slug) return notFound();
return renderHtml(slug, request);
})
.get("/index.json", async ({ request }) => {
const domain = isCustomDomain(request);
if (!domain) return notFound();
const slug = await resolveSlugFromDomain(domain);
if (!slug) return notFound();
return renderJson(slug, request);
})
.get("/index.rss", async ({ request }) => {
const domain = isCustomDomain(request);
if (!domain) return notFound();
const slug = await resolveSlugFromDomain(domain);
if (!slug) return notFound();
return renderRssResp(slug, request);
})
.get("/badge.svg", async ({ request }) => {
const domain = isCustomDomain(request);
if (!domain) return notFound();
const slug = await resolveSlugFromDomain(domain);
if (!slug) return notFound();
const page = await loadStatusPage(slug);
if (!page || !isAuthorised(page, request)) return notFound();
const payload = await cached("payload:" + slug, 15, () => loadPagePayload(slug));
if (!payload) return notFound();
const states = payload.monitors.map((m: any) => m.current_state);
return new Response(renderBadge(page.title, badgeFromState(states)), { headers: { "content-type": "image/svg+xml", "cache-control": page.password_hash ? "private, no-store" : "public, max-age=15" } });
})
// Static expand.js - cached aggressively, hash-busted via query string.
.get("/_static/expand.js", () => new Response(Bun.file(expandJsPath), {