/** * Setup routes - initial system configuration */ import { logActivity } from "#db/activity-log.ts"; import { SetupAlreadyCompleteError, settings } from "#db/settings.ts"; import { t } from "#i18n"; import { applyFlash, parseFormWithCsrf } from "#routes/csrf.ts"; import { errorRedirect, htmlResponse, redirectResponse, } from "#routes/response.ts"; import { createRouter, defineRoutes } from "#routes/router.ts"; import { isValidCountry } from "#shared/countries.ts"; import { signCsrfToken, verifySignedCsrfToken } from "#shared/csrf.ts"; import type { FormParams } from "#shared/form-data.ts"; import { ErrorCode, logDbError, logDebug, logError } from "#shared/logger.ts"; import { getSetupForm } from "#templates/fields/admin.ts"; import { setupCompletePage, setupPage } from "#templates/setup.tsx"; /** Response helper - renders setup page with current stored CSRF token */ const setupResponse = (error?: string) => htmlResponse(setupPage(error)); /** * Validate setup form data (uses form framework + custom validation) */ type SetupValidation = | { valid: true; username: string; password: string; country: string; } | { valid: false; error: string }; const validateSetupForm = (form: FormParams): SetupValidation => { logDebug("Setup", "Validating form data..."); logDebug("Setup", `Form keys: ${Array.from(form.keys()).join(", ")}`); const validation = getSetupForm().validate(form); if (!validation.valid) { logDebug("Setup", `Form framework validation failed: ${validation.error}`); return validation; } const { admin_username: username, admin_password: password, admin_password_confirm: passwordConfirm, } = validation.values; const country = (form.get("country") || "GB").trim().toUpperCase(); // Check Data Controller Agreement acceptance const acceptAgreement = form.get("accept_agreement"); if (acceptAgreement !== "yes") { logDebug("Setup", "Agreement not accepted"); return { error: t("error.agreement_required"), valid: false, }; } if (password.length < 8) { logDebug("Setup", `Password too short: ${password.length}`); return { error: t("error.password_min"), valid: false }; } if (password !== passwordConfirm) { logDebug("Setup", "Passwords do not match"); return { error: t("error.passwords_mismatch"), valid: false }; } if (!isValidCountry(country)) { logDebug("Setup", `Invalid country code: ${country}`); return { error: t("error.country_invalid"), valid: false }; } logDebug("Setup", "Validation passed"); return { country, password, username, valid: true, }; }; /** Setup completion check callback type */ type SetupCheck = () => Promise; /** A setup route handler: the request plus the completion check it guards on. */ type SetupHandler = ( request: Request, isSetupComplete: SetupCheck, ) => Promise; /** * Handle GET /setup/ */ const handleSetupGet: SetupHandler = async (request, isSetupComplete) => { if (await isSetupComplete()) return redirectResponse("/"); await signCsrfToken(); const flash = applyFlash(request); return setupResponse(flash.error); }; /** * Handle POST /setup/ * Validates CSRF token using signed token pattern */ const handleSetupPost: SetupHandler = async (request, isSetupComplete) => { logDebug("Setup", "POST request received"); if (await isSetupComplete()) { logDebug("Setup", "Setup already complete, redirecting"); return redirectResponse("/"); } // Validate signed CSRF token const { form, formCsrf } = await parseFormWithCsrf(request); logDebug( "Setup", `CSRF form present: ${!!formCsrf} length: ${formCsrf.length}`, ); if (!formCsrf || !(await verifySignedCsrfToken(formCsrf))) { logError({ code: ErrorCode.AUTH_CSRF_MISMATCH, detail: "setup form" }); return errorRedirect("/setup/", t("error.csrf_invalid")); } logDebug("Setup", "CSRF validation passed, validating form..."); const validation = validateSetupForm(form); if (!validation.valid) { logError({ code: ErrorCode.VALIDATION_FORM, detail: "setup" }); return errorRedirect("/setup/", validation.error); } logDebug("Setup", "Form validation passed, completing setup..."); try { await settings.setup.complete( validation.username, validation.password, validation.country, ); await logActivity("Initial setup completed"); logDebug("Setup", "Setup completed successfully!"); return redirectResponse("/setup/complete"); } catch (error) { if (error instanceof SetupAlreadyCompleteError) { logDebug("Setup", "Setup already completed by another request"); return redirectResponse("/"); } logDbError("setup completion", error); throw error; } }; /** * Handle GET /setup/complete - setup success page */ const handleSetupComplete = async ( isSetupComplete: SetupCheck, ): Promise => { if (!(await isSetupComplete())) { return redirectResponse("/setup/"); } return htmlResponse(setupCompletePage()); }; /** * Create setup router with injected isSetupComplete dependency * Uses factory pattern since setup routes need to check completion status */ export const createSetupRouter = ( isSetupComplete: () => Promise, ): ReturnType => { const setupRoutes = defineRoutes({ "GET /setup": (request) => handleSetupGet(request, isSetupComplete), "GET /setup/complete": () => handleSetupComplete(isSetupComplete), "POST /setup": (request) => handleSetupPost(request, isSetupComplete), }); return createRouter(setupRoutes); };