2026-05-09 23:09:13 +00:00
|
|
|
/**
|
|
|
|
|
* First-run setup routes.
|
|
|
|
|
*/
|
2026-05-10 00:50:16 +00:00
|
|
|
import { type H3, readBody } from "h3";
|
|
|
|
|
import { htmlPage } from "./html-response.js";
|
2026-05-09 23:09:13 +00:00
|
|
|
import type { AdminDeps } from "./index.js";
|
|
|
|
|
import { SetupPage } from "../../web-templates/auth-pages.js";
|
2026-05-26 12:03:58 +00:00
|
|
|
import { SetupBody, validateBody } from "../../shared/api-schemas.js";
|
2026-05-09 23:09:13 +00:00
|
|
|
|
|
|
|
|
export function registerSetupRoutes(app: H3, deps: AdminDeps): void {
|
2026-05-23 00:07:44 +00:00
|
|
|
app.get("/setup", async () => {
|
|
|
|
|
if (await deps.repo.isSetupComplete()) {
|
2026-05-09 23:09:13 +00:00
|
|
|
return new Response(null, { status: 302, headers: { location: "/admin/" } });
|
|
|
|
|
}
|
2026-05-10 00:50:16 +00:00
|
|
|
return htmlPage(SetupPage({}));
|
2026-05-09 23:09:13 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post("/setup", async (event) => {
|
2026-05-23 00:07:44 +00:00
|
|
|
if (await deps.repo.isSetupComplete()) {
|
2026-05-09 23:09:13 +00:00
|
|
|
return new Response(null, { status: 302, headers: { location: "/admin/" } });
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 12:03:58 +00:00
|
|
|
let body: { username: string; password: string };
|
|
|
|
|
try {
|
|
|
|
|
body = validateBody(SetupBody, await readBody(event));
|
|
|
|
|
} catch {
|
|
|
|
|
return htmlPage(SetupPage({ error: "Username (3-64 chars) and password (12+ chars) required.", username: "" }));
|
|
|
|
|
}
|
|
|
|
|
const username = body.username.trim();
|
|
|
|
|
const password = body.password;
|
2026-05-09 23:09:13 +00:00
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
2026-05-26 12:03:58 +00:00
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
2026-05-09 23:09:13 +00:00
|
|
|
errors.push("Username may only contain letters, digits, underscore, or hyphen.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
2026-05-10 00:50:16 +00:00
|
|
|
return htmlPage(SetupPage({ error: errors.join(" "), username }));
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hash = await deps.auth.hashPassword(password);
|
2026-05-23 00:07:44 +00:00
|
|
|
await deps.repo.createUser({ username, password_hash: hash, role: "admin" });
|
2026-05-09 23:09:13 +00:00
|
|
|
|
|
|
|
|
const clusterKey = deps.secrets.generateClusterKey();
|
|
|
|
|
const encryptedCluster = deps.secrets.encryptString(clusterKey, "cluster");
|
2026-05-23 00:07:44 +00:00
|
|
|
await deps.repo.setSetupExtra("cluster_key_encrypted", encryptedCluster);
|
|
|
|
|
await deps.repo.markClusterKeyProvisioned();
|
2026-05-09 23:09:13 +00:00
|
|
|
|
2026-05-10 19:39:09 +00:00
|
|
|
// Setup only creates admin user + cluster key.
|
|
|
|
|
// Displays are created when kiosks are paired (kiosk reports HDMI ports).
|
|
|
|
|
// Layouts are created by admin after pairing.
|
2026-05-23 00:07:44 +00:00
|
|
|
await deps.repo.markSetupComplete();
|
2026-05-09 23:09:13 +00:00
|
|
|
|
|
|
|
|
return new Response(null, {
|
|
|
|
|
status: 302,
|
|
|
|
|
headers: { location: "/auth/login?welcome=1" },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|