From 7e4df5e50b37dcba94c9b2dfab37335cb0871592 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:53:11 +0100 Subject: [PATCH] removed redirect feature --- app/(dashboard)/DashboardLayoutClient.tsx | 2 - app/(dashboard)/page.tsx | 9 +- app/(dashboard)/redirects/RedirectsClient.tsx | 536 ------------------ app/(dashboard)/redirects/actions.ts | 86 --- app/(dashboard)/redirects/page.tsx | 9 - app/api/instances/sync/route.ts | 19 +- drizzle/0006_remove_redirects.sql | 1 + src/lib/caddy.ts | 63 +- src/lib/db/schema.ts | 13 - src/lib/instance-sync.ts | 20 +- src/lib/models/redirect-hosts.ts | 139 ----- 11 files changed, 11 insertions(+), 886 deletions(-) delete mode 100644 app/(dashboard)/redirects/RedirectsClient.tsx delete mode 100644 app/(dashboard)/redirects/actions.ts delete mode 100644 app/(dashboard)/redirects/page.tsx create mode 100644 drizzle/0006_remove_redirects.sql delete mode 100644 src/lib/models/redirect-hosts.ts diff --git a/app/(dashboard)/DashboardLayoutClient.tsx b/app/(dashboard)/DashboardLayoutClient.tsx index 0716b73e..bb399379 100644 --- a/app/(dashboard)/DashboardLayoutClient.tsx +++ b/app/(dashboard)/DashboardLayoutClient.tsx @@ -22,7 +22,6 @@ import { import MenuIcon from "@mui/icons-material/Menu"; import DashboardIcon from "@mui/icons-material/Dashboard"; import DnsIcon from "@mui/icons-material/Dns"; -import SwapHorizIcon from "@mui/icons-material/SwapHoriz"; import SecurityIcon from "@mui/icons-material/Security"; import ShieldIcon from "@mui/icons-material/Shield"; import SettingsIcon from "@mui/icons-material/Settings"; @@ -39,7 +38,6 @@ type User = { const NAV_ITEMS = [ { href: "/", label: "Overview", icon: DashboardIcon }, { href: "/proxy-hosts", label: "Proxy Hosts", icon: DnsIcon }, - { href: "/redirects", label: "Redirects", icon: SwapHorizIcon }, { href: "/access-lists", label: "Access Lists", icon: SecurityIcon }, { href: "/certificates", label: "Certificates", icon: ShieldIcon }, { href: "/settings", label: "Settings", icon: SettingsIcon }, diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index d5cb2db6..9ed3a120 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -5,12 +5,10 @@ import { accessLists, auditEvents, certificates, - proxyHosts, - redirectHosts + proxyHosts } from "@/src/lib/db/schema"; import { count, desc } from "drizzle-orm"; import SwapHorizIcon from "@mui/icons-material/SwapHoriz"; -import TurnRightIcon from "@mui/icons-material/TurnRight"; import SecurityIcon from "@mui/icons-material/Security"; import VpnKeyIcon from "@mui/icons-material/VpnKey"; import { ReactNode } from "react"; @@ -23,21 +21,18 @@ type StatCard = { }; async function loadStats(): Promise { - const [proxyHostCountResult, redirectHostCountResult, certificateCountResult, accessListCountResult] = + const [proxyHostCountResult, certificateCountResult, accessListCountResult] = await Promise.all([ db.select({ value: count() }).from(proxyHosts), - db.select({ value: count() }).from(redirectHosts), db.select({ value: count() }).from(certificates), db.select({ value: count() }).from(accessLists) ]); const proxyHostsCount = proxyHostCountResult[0]?.value ?? 0; - const redirectHostsCount = redirectHostCountResult[0]?.value ?? 0; const certificatesCount = certificateCountResult[0]?.value ?? 0; const accessListsCount = accessListCountResult[0]?.value ?? 0; return [ { label: "Proxy Hosts", icon: , count: proxyHostsCount, href: "/proxy-hosts" }, - { label: "Redirects", icon: , count: redirectHostsCount, href: "/redirects" }, { label: "Certificates", icon: , count: certificatesCount, href: "/certificates" }, { label: "Access Lists", icon: , count: accessListsCount, href: "/access-lists" } ]; diff --git a/app/(dashboard)/redirects/RedirectsClient.tsx b/app/(dashboard)/redirects/RedirectsClient.tsx deleted file mode 100644 index dea4c4da..00000000 --- a/app/(dashboard)/redirects/RedirectsClient.tsx +++ /dev/null @@ -1,536 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { useRouter } from "next/navigation"; -import { - Alert, - Box, - Button, - Card, - Checkbox, - Chip, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControlLabel, - IconButton, - Stack, - Switch, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Typography, - Tooltip -} from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/Delete"; -import CloseIcon from "@mui/icons-material/Close"; -import SearchIcon from "@mui/icons-material/Search"; -import { useFormState } from "react-dom"; -import type { RedirectHost } from "@/src/lib/models/redirect-hosts"; -import { INITIAL_ACTION_STATE } from "@/src/lib/actions"; -import { createRedirectAction, deleteRedirectAction, updateRedirectAction, toggleRedirectAction } from "./actions"; - -type Props = { - redirects: RedirectHost[]; -}; - -export default function RedirectsClient({ redirects }: Props) { - const [createOpen, setCreateOpen] = useState(false); - const [editRedirect, setEditRedirect] = useState(null); - const [deleteRedirect, setDeleteRedirect] = useState(null); - const [searchTerm, setSearchTerm] = useState(""); - - const filteredRedirects = useMemo(() => { - if (!searchTerm.trim()) return redirects; - - const search = searchTerm.toLowerCase(); - return redirects.filter((redirect) => { - // Search in name - if (redirect.name.toLowerCase().includes(search)) return true; - - // Search in domains - if (redirect.domains.some(domain => domain.toLowerCase().includes(search))) return true; - - // Search in destination - if (redirect.destination.toLowerCase().includes(search)) return true; - - // Search in status code - if (redirect.status_code.toString().includes(search)) return true; - - return false; - }); - }, [redirects, searchTerm]); - - const handleToggleEnabled = async (id: number, enabled: boolean) => { - await toggleRedirectAction(id, enabled); - }; - - return ( - - - - - Redirects - - - Return HTTP 301/302 responses to guide clients toward canonical hosts. - - - - - - setSearchTerm(e.target.value)} - slotProps={{ - input: { - startAdornment: - } - }} - sx={{ - maxWidth: 500, - "& .MuiOutlinedInput-root": { - bgcolor: "rgba(20, 20, 22, 0.6)", - "&:hover": { - bgcolor: "rgba(20, 20, 22, 0.8)" - } - } - }} - /> - - - - - - Name - Domains - Destination - Status Code - Status - Actions - - - - {filteredRedirects.length === 0 ? ( - - - {searchTerm ? "No redirects match your search." : "No redirects configured. Click \"Create Redirect\" to add one."} - - - ) : ( - filteredRedirects.map((redirect) => ( - - - - {redirect.name} - - - - - {redirect.domains.join(", ")} - - - - - {redirect.destination} - - - - - - - handleToggleEnabled(redirect.id, e.target.checked)} - size="small" - sx={{ - "& .MuiSwitch-switchBase.Mui-checked": { - color: "rgba(34, 197, 94, 1)" - }, - "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": { - backgroundColor: "rgba(34, 197, 94, 0.5)" - } - }} - /> - - - - - setEditRedirect(redirect)} - sx={{ - color: "rgba(99, 102, 241, 0.8)", - "&:hover": { bgcolor: "rgba(99, 102, 241, 0.1)" } - }} - > - - - - - setDeleteRedirect(redirect)} - sx={{ - color: "rgba(239, 68, 68, 0.8)", - "&:hover": { bgcolor: "rgba(239, 68, 68, 0.1)" } - }} - > - - - - - - - )) - )} - -
-
- - setCreateOpen(false)} /> - - {editRedirect && ( - setEditRedirect(null)} - /> - )} - - {deleteRedirect && ( - setDeleteRedirect(null)} - /> - )} -
- ); -} - -function CreateRedirectDialog({ open, onClose }: { open: boolean; onClose: () => void }) { - const [state, formAction] = useFormState(createRedirectAction, INITIAL_ACTION_STATE); - const router = useRouter(); - - useEffect(() => { - if (state.status === "success") { - router.refresh(); - setTimeout(onClose, 1000); - } - }, [state.status, router, onClose]); - - return ( - - - - Create Redirect - - - - - - - - {state.status !== "idle" && state.message && ( - - {state.message} - - )} - - - - - - - - - - - - - - - - ); -} - -function EditRedirectDialog({ - open, - redirect, - onClose -}: { - open: boolean; - redirect: RedirectHost; - onClose: () => void; -}) { - const [state, formAction] = useFormState(updateRedirectAction.bind(null, redirect.id), INITIAL_ACTION_STATE); - const router = useRouter(); - - useEffect(() => { - if (state.status === "success") { - router.refresh(); - setTimeout(onClose, 1000); - } - }, [state.status, router, onClose]); - - return ( - - - - Edit Redirect - - - - - - - - {state.status !== "idle" && state.message && ( - - {state.message} - - )} - - - - - - - - - - - - - - - - ); -} - -function DeleteRedirectDialog({ - open, - redirect, - onClose -}: { - open: boolean; - redirect: RedirectHost; - onClose: () => void; -}) { - const [state, formAction] = useFormState(deleteRedirectAction.bind(null, redirect.id), INITIAL_ACTION_STATE); - const router = useRouter(); - - useEffect(() => { - if (state.status === "success") { - router.refresh(); - setTimeout(onClose, 1000); - } - }, [state.status, router, onClose]); - - return ( - - - - Delete Redirect - - - - - - - - {state.status !== "idle" && state.message && ( - - {state.message} - - )} - - Are you sure you want to delete the redirect {redirect.name}? - - - This will remove the redirect from: - - - - • Domains: {redirect.domains.join(", ")} - - - • To: {redirect.destination} - - - - This action cannot be undone. - - - - - -
- -
-
-
- ); -} - -function HiddenCheckboxField({ name, defaultChecked, label }: { name: string; defaultChecked: boolean; label: string }) { - return ( - - - - } - label={{label}} - /> - - ); -} diff --git a/app/(dashboard)/redirects/actions.ts b/app/(dashboard)/redirects/actions.ts deleted file mode 100644 index 54fd5f5f..00000000 --- a/app/(dashboard)/redirects/actions.ts +++ /dev/null @@ -1,86 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { requireAdmin } from "@/src/lib/auth"; -import { createRedirectHost, deleteRedirectHost, updateRedirectHost } from "@/src/lib/models/redirect-hosts"; -import { actionSuccess, actionError, type ActionState } from "@/src/lib/actions"; - -function parseList(value: FormDataEntryValue | null): string[] { - if (!value || typeof value !== "string") { - return []; - } - return value - .replace(/\n/g, ",") - .split(",") - .map((item) => item.trim()) - .filter(Boolean); -} - -export async function createRedirectAction(_prevState: ActionState, formData: FormData): Promise { - try { - const session = await requireAdmin(); - const userId = Number(session.user.id); - await createRedirectHost( - { - name: String(formData.get("name") ?? "Redirect"), - domains: parseList(formData.get("domains")), - destination: String(formData.get("destination") ?? ""), - status_code: formData.get("status_code") ? Number(formData.get("status_code")) : 302, - preserve_query: formData.get("preserve_query") === "on", - enabled: formData.has("enabled") ? formData.get("enabled") === "on" : true - }, - userId - ); - revalidatePath("/redirects"); - return actionSuccess("Redirect created successfully"); - } catch (error) { - return actionError(error, "Failed to create redirect"); - } -} - -export async function updateRedirectAction(id: number, _prevState: ActionState, formData: FormData): Promise { - try { - const session = await requireAdmin(); - const userId = Number(session.user.id); - await updateRedirectHost( - id, - { - name: formData.get("name") ? String(formData.get("name")) : undefined, - domains: formData.get("domains") ? parseList(formData.get("domains")) : undefined, - destination: formData.get("destination") ? String(formData.get("destination")) : undefined, - status_code: formData.get("status_code") ? Number(formData.get("status_code")) : undefined, - preserve_query: formData.has("preserve_query_present") ? formData.get("preserve_query") === "on" : undefined, - enabled: formData.has("enabled_present") ? formData.get("enabled") === "on" : undefined - }, - userId - ); - revalidatePath("/redirects"); - return actionSuccess("Redirect updated successfully"); - } catch (error) { - return actionError(error, "Failed to update redirect"); - } -} - -export async function deleteRedirectAction(id: number, _prevState: ActionState): Promise { - try { - const session = await requireAdmin(); - const userId = Number(session.user.id); - await deleteRedirectHost(id, userId); - revalidatePath("/redirects"); - return actionSuccess("Redirect deleted successfully"); - } catch (error) { - return actionError(error, "Failed to delete redirect"); - } -} - -export async function toggleRedirectAction(id: number, enabled: boolean): Promise { - try { - const session = await requireAdmin(); - const userId = Number(session.user.id); - await updateRedirectHost(id, { enabled }, userId); - revalidatePath("/redirects"); - return actionSuccess(`Redirect ${enabled ? "enabled" : "disabled"}.`); - } catch (error) { - return actionError(error, "Failed to toggle redirect"); - } -} diff --git a/app/(dashboard)/redirects/page.tsx b/app/(dashboard)/redirects/page.tsx deleted file mode 100644 index 973ee93c..00000000 --- a/app/(dashboard)/redirects/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import RedirectsClient from "./RedirectsClient"; -import { listRedirectHosts } from "@/src/lib/models/redirect-hosts"; -import { requireAdmin } from "@/src/lib/auth"; - -export default async function RedirectsPage() { - await requireAdmin(); - const redirects = await listRedirectHosts(); - return ; -} diff --git a/app/api/instances/sync/route.ts b/app/api/instances/sync/route.ts index 9a1b16f9..d4b75697 100644 --- a/app/api/instances/sync/route.ts +++ b/app/api/instances/sync/route.ts @@ -145,22 +145,6 @@ function isProxyHost(value: unknown): value is SyncPayload["data"]["proxyHosts"] ); } -function isRedirectHost(value: unknown): value is SyncPayload["data"]["redirectHosts"][number] { - if (!isRecord(value)) return false; - return ( - isNumber(value.id) && - isString(value.name) && - isString(value.domains) && - isString(value.destination) && - isNumber(value.statusCode) && - isBoolean(value.preserveQuery) && - isBoolean(value.enabled) && - isNullableNumber(value.createdBy) && - isString(value.createdAt) && - isString(value.updatedAt) - ); -} - /** * Validates that the payload has the expected structure for syncing */ @@ -197,8 +181,7 @@ function isValidSyncPayload(payload: unknown): payload is SyncPayload { validateArray(d.certificates, isCertificate) && validateArray(d.accessLists, isAccessList) && validateArray(d.accessListEntries, isAccessListEntry) && - validateArray(d.proxyHosts, isProxyHost) && - validateArray(d.redirectHosts, isRedirectHost) + validateArray(d.proxyHosts, isProxyHost) ); } diff --git a/drizzle/0006_remove_redirects.sql b/drizzle/0006_remove_redirects.sql new file mode 100644 index 00000000..6667d62e --- /dev/null +++ b/drizzle/0006_remove_redirects.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS redirect_hosts; diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 371e433e..3199d015 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -8,8 +8,7 @@ import { syncInstances } from "./instance-sync"; import { accessListEntries, certificates, - proxyHosts, - redirectHosts + proxyHosts } from "./db/schema"; const CERTS_DIR = process.env.CERTS_DIRECTORY || join(process.cwd(), "data", "certs"); @@ -144,16 +143,6 @@ type LoadBalancerRouteConfig = { } | null; }; -type RedirectHostRow = { - id: number; - name: string; - domains: string; - destination: string; - status_code: number; - preserve_query: number; - enabled: number; -}; - type AccessListEntryRow = { access_list_id: number; username: string; @@ -688,30 +677,6 @@ function buildProxyRoutes( return routes; } -function buildRedirectRoutes(rows: RedirectHostRow[]): CaddyHttpRoute[] { - return rows - .filter((row) => Boolean(row.enabled)) - .map((row) => { - const domains = parseJson(row.domains, []); - const preserveQuery = Boolean(row.preserve_query); - const location = preserveQuery ? `${row.destination}{uri}` : row.destination; - return { - match: [{ host: domains }], - handle: [ - { - handler: "static_response", - status_code: row.status_code, - headers: { - Location: [location], - "Strict-Transport-Security": ["max-age=63072000"] - } - } - ], - terminal: true - }; - }); -} - function buildTlsConnectionPolicies( usage: Map, managedCertificatesWithAutomation: Set, @@ -907,7 +872,7 @@ async function buildTlsAutomation( } async function buildCaddyDocument() { - const [proxyHostRecords, redirectHostRecords, certRows, accessListEntryRecords] = await Promise.all([ + const [proxyHostRecords, certRows, accessListEntryRecords] = await Promise.all([ db .select({ id: proxyHosts.id, @@ -926,17 +891,6 @@ async function buildCaddyDocument() { enabled: proxyHosts.enabled }) .from(proxyHosts), - db - .select({ - id: redirectHosts.id, - name: redirectHosts.name, - domains: redirectHosts.domains, - destination: redirectHosts.destination, - statusCode: redirectHosts.statusCode, - preserveQuery: redirectHosts.preserveQuery, - enabled: redirectHosts.enabled - }) - .from(redirectHosts), db .select({ id: certificates.id, @@ -975,16 +929,6 @@ async function buildCaddyDocument() { enabled: h.enabled ? 1 : 0 })); - const redirectHostRows: RedirectHostRow[] = redirectHostRecords.map((h) => ({ - id: h.id, - name: h.name, - domains: h.domains, - destination: h.destination, - status_code: h.statusCode, - preserve_query: h.preserveQuery ? 1 : 0, - enabled: h.enabled ? 1 : 0 - })); - const certRowsMapped: CertificateRow[] = certRows.map((c: typeof certRows[0]) => ({ id: c.id, name: c.name, @@ -1023,8 +967,7 @@ async function buildCaddyDocument() { ); const httpRoutes: CaddyHttpRoute[] = [ - ...buildProxyRoutes(proxyHostRows, accessMap, readyCertificates, autoManagedDomains), - ...buildRedirectRoutes(redirectHostRows) + ...buildProxyRoutes(proxyHostRows, accessMap, readyCertificates, autoManagedDomains) ]; const hasTls = tlsConnectionPolicies.length > 0; diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 5b200050..bfebe890 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -150,19 +150,6 @@ export const proxyHosts = sqliteTable("proxy_hosts", { .default(false) }); -export const redirectHosts = sqliteTable("redirect_hosts", { - id: integer("id").primaryKey({ autoIncrement: true }), - name: text("name").notNull(), - domains: text("domains").notNull(), - destination: text("destination").notNull(), - statusCode: integer("status_code").notNull().default(302), - preserveQuery: integer("preserve_query", { mode: "boolean" }).notNull().default(true), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - createdBy: integer("created_by").references(() => users.id, { onDelete: "set null" }), - createdAt: text("created_at").notNull(), - updatedAt: text("updated_at").notNull() -}); - export const apiTokens = sqliteTable( "api_tokens", { diff --git a/src/lib/instance-sync.ts b/src/lib/instance-sync.ts index 9640b94b..b2d603ac 100644 --- a/src/lib/instance-sync.ts +++ b/src/lib/instance-sync.ts @@ -1,5 +1,5 @@ import db, { nowIso } from "./db"; -import { accessListEntries, accessLists, certificates, proxyHosts, redirectHosts } from "./db/schema"; +import { accessListEntries, accessLists, certificates, proxyHosts } from "./db/schema"; import { getSetting, setSetting } from "./settings"; import { recordInstanceSyncResult, updateInstance } from "./models/instances"; import { decryptSecret, encryptSecret, isEncryptedSecret } from "./secret"; @@ -23,7 +23,6 @@ export type SyncPayload = { accessLists: Array; accessListEntries: Array; proxyHosts: Array; - redirectHosts: Array; }; }; @@ -229,12 +228,11 @@ export async function clearSyncedSetting(key: string): Promise { } export async function buildSyncPayload(): Promise { - const [certRows, accessListRows, accessEntryRows, proxyRows, redirectRows] = await Promise.all([ + const [certRows, accessListRows, accessEntryRows, proxyRows] = await Promise.all([ db.select().from(certificates), db.select().from(accessLists), db.select().from(accessListEntries), - db.select().from(proxyHosts), - db.select().from(redirectHosts) + db.select().from(proxyHosts) ]); const settings = { @@ -256,11 +254,6 @@ export async function buildSyncPayload(): Promise { createdBy: null })); - const sanitizedRedirects = redirectRows.map((row) => ({ - ...row, - createdBy: null - })); - const sanitizedProxyHosts = proxyRows.map((row) => ({ ...row, ownerUserId: null @@ -273,8 +266,7 @@ export async function buildSyncPayload(): Promise { certificates: sanitizedCertificates, accessLists: sanitizedAccessLists, accessListEntries: accessEntryRows, - proxyHosts: sanitizedProxyHosts, - redirectHosts: sanitizedRedirects + proxyHosts: sanitizedProxyHosts } }; } @@ -407,7 +399,6 @@ export async function applySyncPayload(payload: SyncPayload) { // better-sqlite3 is synchronous, so transaction callback must be synchronous db.transaction((tx) => { tx.delete(proxyHosts).run(); - tx.delete(redirectHosts).run(); tx.delete(accessListEntries).run(); tx.delete(accessLists).run(); tx.delete(certificates).run(); @@ -424,8 +415,5 @@ export async function applySyncPayload(payload: SyncPayload) { if (payload.data.proxyHosts.length > 0) { tx.insert(proxyHosts).values(payload.data.proxyHosts).run(); } - if (payload.data.redirectHosts.length > 0) { - tx.insert(redirectHosts).values(payload.data.redirectHosts).run(); - } }); } diff --git a/src/lib/models/redirect-hosts.ts b/src/lib/models/redirect-hosts.ts deleted file mode 100644 index aac97d94..00000000 --- a/src/lib/models/redirect-hosts.ts +++ /dev/null @@ -1,139 +0,0 @@ -import db, { nowIso, toIso } from "../db"; -import { logAuditEvent } from "../audit"; -import { applyCaddyConfig } from "../caddy"; -import { redirectHosts } from "../db/schema"; -import { desc, eq } from "drizzle-orm"; - -export type RedirectHost = { - id: number; - name: string; - domains: string[]; - destination: string; - status_code: number; - preserve_query: boolean; - enabled: boolean; - created_at: string; - updated_at: string; -}; - -export type RedirectHostInput = { - name: string; - domains: string[]; - destination: string; - status_code?: number; - preserve_query?: boolean; - enabled?: boolean; -}; - -type RedirectHostRow = typeof redirectHosts.$inferSelect; - -function parseDbRecord(record: RedirectHostRow): RedirectHost { - return { - id: record.id, - name: record.name, - domains: JSON.parse(record.domains), - destination: record.destination, - status_code: record.statusCode, - preserve_query: record.preserveQuery, - enabled: record.enabled, - created_at: toIso(record.createdAt)!, - updated_at: toIso(record.updatedAt)! - }; -} - -export async function listRedirectHosts(): Promise { - const records = await db.select().from(redirectHosts).orderBy(desc(redirectHosts.createdAt)); - return records.map(parseDbRecord); -} - -export async function getRedirectHost(id: number): Promise { - const record = await db.query.redirectHosts.findFirst({ - where: (table, { eq }) => eq(table.id, id) - }); - return record ? parseDbRecord(record) : null; -} - -export async function createRedirectHost(input: RedirectHostInput, actorUserId: number) { - if (!input.domains || input.domains.length === 0) { - throw new Error("At least one domain is required"); - } - - const now = nowIso(); - const [record] = await db - .insert(redirectHosts) - .values({ - name: input.name.trim(), - domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))), - destination: input.destination.trim(), - statusCode: input.status_code ?? 302, - preserveQuery: input.preserve_query ?? true, - enabled: input.enabled ?? true, - createdAt: now, - updatedAt: now, - createdBy: actorUserId - }) - .returning(); - - if (!record) { - throw new Error("Failed to create redirect host"); - } - - logAuditEvent({ - userId: actorUserId, - action: "create", - entityType: "redirect_host", - entityId: record.id, - summary: `Created redirect ${input.name}` - }); - await applyCaddyConfig(); - return (await getRedirectHost(record.id))!; -} - -export async function updateRedirectHost(id: number, input: Partial, actorUserId: number) { - const existing = await getRedirectHost(id); - if (!existing) { - throw new Error("Redirect host not found"); - } - - const now = nowIso(); - await db - .update(redirectHosts) - .set({ - name: input.name ?? existing.name, - domains: input.domains ? JSON.stringify(Array.from(new Set(input.domains))) : JSON.stringify(existing.domains), - destination: input.destination ?? existing.destination, - statusCode: input.status_code ?? existing.status_code, - preserveQuery: input.preserve_query ?? existing.preserve_query, - enabled: input.enabled ?? existing.enabled, - updatedAt: now - }) - .where(eq(redirectHosts.id, id)); - - logAuditEvent({ - userId: actorUserId, - action: "update", - entityType: "redirect_host", - entityId: id, - summary: `Updated redirect ${input.name ?? existing.name}` - }); - await applyCaddyConfig(); - return (await getRedirectHost(id))!; -} - -export async function deleteRedirectHost(id: number, actorUserId: number) { - const existing = await getRedirectHost(id); - if (!existing) { - throw new Error("Redirect host not found"); - } - - await db.delete(redirectHosts).where(eq(redirectHosts.id, id)); - - logAuditEvent({ - userId: actorUserId, - action: "delete", - entityType: "redirect_host", - entityId: id, - summary: `Deleted redirect ${existing.name}` - }); - await applyCaddyConfig(); -}