import { Router } from "express"; import { eq } from "drizzle-orm"; import { z } from "zod"; import { db } from "../db/client.js"; import { domains } from "../db/schema.js"; import { requireAuth, requireRole } from "../auth/middleware.js"; import { recordAudit } from "../services/audit.js"; import { addManualDomain, checkDomain, domainStatus, isDomainCheckRunning, refreshAllDomains, type DomainRow } from "../services/domainMonitor.js"; import { getSettings } from "../services/settingsStore.js"; import { asyncHandler } from "../utils/asyncHandler.js"; export const domainsRouter = Router(); domainsRouter.use(requireAuth); function present(row: DomainRow, warnDays: number) { return { ...row, ...domainStatus(row, warnDays) }; } async function listPresented() { const { healthChecks } = await getSettings(); const rows = await db.select().from(domains).orderBy(domains.name); return { domains: rows.map((r) => present(r, healthChecks.domainWarnDays)), warnDays: healthChecks.domainWarnDays, checking: isDomainCheckRunning() }; } domainsRouter.get("/", asyncHandler(async (_req, res) => { res.json(await listPresented()); })); const addSchema = z.object({ name: z.string().min(1).max(253) }); domainsRouter.post("/", requireRole("operator"), asyncHandler(async (req, res) => { const parsed = addSchema.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: "invalid_body", message: "Enter a domain name.", details: parsed.error.flatten() }); const result = await addManualDomain(parsed.data.name); if (!result.ok) return res.status(result.status).json({ error: "cannot_add", message: result.message }); await recordAudit({ actor: req.currentUser!, category: "domain", action: "add", targetType: "domain", targetId: result.row.id, detail: { name: result.row.name, expiresAt: result.row.expiresAt }, }); const { healthChecks } = await getSettings(); res.status(201).json({ domain: present(result.row, healthChecks.domainWarnDays), resolvedFrom: result.resolvedFrom }); })); // Registered before "/:id/..." so "refresh" isn't taken for an id. domainsRouter.post("/refresh", requireRole("operator"), asyncHandler(async (req, res) => { const started = await refreshAllDomains(); if (!started) return res.status(409).json({ error: "already_running", message: "A domain check is already running." }); await recordAudit({ actor: req.currentUser!, category: "domain", action: "refresh_all", targetType: "domain", detail: {} }); res.json(await listPresented()); })); domainsRouter.post("/:id/check", requireRole("operator"), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ error: "invalid_id" }); const row = await checkDomain(id); if (!row) return res.status(404).json({ error: "not_found" }); await recordAudit({ actor: req.currentUser!, category: "domain", action: "check", targetType: "domain", targetId: id, detail: { name: row.name, error: row.lastCheckError }, }); const { healthChecks } = await getSettings(); res.json({ domain: present(row, healthChecks.domainWarnDays) }); })); domainsRouter.delete("/:id", requireRole("operator"), asyncHandler(async (req, res) => { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ error: "invalid_id" }); const [row] = await db.select().from(domains).where(eq(domains.id, id)).limit(1); if (!row) return res.status(404).json({ error: "not_found" }); if (row.origin === "zone") { return res.status(409).json({ error: "zone_domain", message: `${row.name} is tracked because a DNS zone for it is configured. It goes away by itself when that zone is removed.`, }); } await db.delete(domains).where(eq(domains.id, id)); await recordAudit({ actor: req.currentUser!, category: "domain", action: "remove", targetType: "domain", targetId: id, detail: { name: row.name } }); res.status(204).end(); }));