src/features/setup.ts
Rama main · réplica de lectura del repositorio original.
/**
* 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<boolean>;
/** A setup route handler: the request plus the completion check it guards on. */
type SetupHandler = (
request: Request,
isSetupComplete: SetupCheck,
) => Promise<Response>;
/**
* 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<Response> => {
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<boolean>,
): ReturnType<typeof createRouter> => {
const setupRoutes = defineRoutes({
"GET /setup": (request) => handleSetupGet(request, isSetupComplete),
"GET /setup/complete": () => handleSetupComplete(isSetupComplete),
"POST /setup": (request) => handleSetupPost(request, isSetupComplete),
});
return createRouter(setupRoutes);
};