import { randomBytes, scryptSync } from "node:crypto";
import { initDb, query } from "./db.ts";
import type { SessionUser } from "../utils.ts";
/**
* Local development convenience: while the Vite dev server is running, any
* request without a session is served as a seeded admin so the game and the
* administration screens are reachable without signing in.
*
* `import.meta.env.DEV` is statically replaced with `false` in the production
* bundle, so none of this can ever run on a deployed build.
*/
export const DEV_USER_EMAIL = "dev@localhost";
export const DEV_USER_NAME = "Dev Admin";
const DEV_LOGOUT_COOKIE = "profectus_dev_signed_out";
export function devAutoLoginEnabled(): boolean {
return import.meta.env.DEV && Deno.env.get("DEV_AUTOLOGIN") !== "off";
}
function hasCookie(req: Request, name: string): boolean {
const header = req.headers.get("cookie");
if (!header) return false;
return header.split(";").some((part) => {
const idx = part.indexOf("=");
return idx !== -1 && part.slice(0, idx).trim() === name;
});
}
/** True when the developer explicitly used /logout and wants the login page. */
export function devSignedOut(req: Request): boolean {
return hasCookie(req, DEV_LOGOUT_COOKIE);
}
export function devSignOutCookie(): string {
return `${DEV_LOGOUT_COOKIE}=1; Path=/; SameSite=Lax; Max-Age=${
60 * 60 * 24
}`;
}
export function clearDevSignOutCookie(): string {
return `${DEV_LOGOUT_COOKIE}=; Path=/; SameSite=Lax; Max-Age=0`;
}
let cached: SessionUser | null = null;
/** Finds or creates the local dev account and guarantees it holds admin. */
export async function ensureDevAdmin(): Promise<SessionUser> {
if (cached) return cached;
await initDb();
const existing = await query("SELECT id FROM users WHERE email = ?", [
DEV_USER_EMAIL,
]);
let id = existing.rows[0] ? Number(existing.rows[0].id) : null;
if (id === null) {
// The password is unusable on purpose: this account exists for the local
// dev server only and is never meant to be signed into by hand.
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(randomBytes(32), salt, 64).toString("hex");
const inserted = await query(
`INSERT INTO users (email, name, password_hash, role)
VALUES (?, ?, ?, 'admin')`,
[DEV_USER_EMAIL, DEV_USER_NAME, `${salt}:${hash}`],
);
id = Number(inserted.lastInsertRowid);
} else {
await query("UPDATE users SET role = 'admin' WHERE id = ?", [id]);
}
cached = { id, email: DEV_USER_EMAIL, name: DEV_USER_NAME, role: "admin" };
return cached;
}