import crypto from "node:crypto"; import type { Db } from "./db.js"; import { nowIso } from "./db.js"; import { newId, ID_PREFIX } from "./domain/ids.js"; const SCRYPT_OPTS = { N: 16384, r: 8, p: 1 }; const SESSION_TTL_MS = 14 * 24 * 60 * 60 * 1000; export function hashPassword(password: string): string { const salt = crypto.randomBytes(16).toString("hex"); const hash = crypto.scryptSync(password, salt, 64, SCRYPT_OPTS).toString("hex"); return `scrypt$${salt}$${hash}`; } export function verifyPassword(password: string, stored: string): boolean { const [scheme, salt, hash] = stored.split("$"); if (scheme !== "scrypt" || !salt || !hash) return false; const candidate = crypto.scryptSync(password, salt, 64, SCRYPT_OPTS); const expected = Buffer.from(hash, "hex"); return candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected); } export function createUser( db: Db, input: { login: string; displayName: string; role: "operator" | "approver" | "admin"; password: string } ): string { const id = newId(ID_PREFIX.user); db.prepare( `INSERT INTO users (id, login, display_name, role, password_hash, created_at) VALUES (?, ?, ?, ?, ?, ?)` ).run(id, input.login, input.displayName, input.role, hashPassword(input.password), nowIso()); return id; } export interface SessionUser { id: string; login: string; displayName: string; role: string; } export function login(db: Db, loginName: string, password: string): { sessionId: string; user: SessionUser } | null { const row = db.prepare("SELECT * FROM users WHERE login = ?").get(loginName) as unknown as | { id: string; login: string; display_name: string; role: string; password_hash: string } | undefined; if (!row || !verifyPassword(password, row.password_hash)) return null; const sessionId = newId(ID_PREFIX.session); const expires = new Date(Date.now() + SESSION_TTL_MS).toISOString(); db.prepare("INSERT INTO sessions (id, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)").run( sessionId, row.id, expires, nowIso() ); return { sessionId, user: { id: row.id, login: row.login, displayName: row.display_name, role: row.role } }; } export function resolveSession(db: Db, sessionId: string | undefined): SessionUser | null { if (!sessionId) return null; const row = db .prepare( `SELECT u.id, u.login, u.display_name, u.role FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = ? AND s.expires_at > ?` ) .get(sessionId, nowIso()) as unknown as | { id: string; login: string; display_name: string; role: string } | undefined; if (!row) return null; return { id: row.id, login: row.login, displayName: row.display_name, role: row.role }; } export function logout(db: Db, sessionId: string): void { db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId); } export function countUsers(db: Db): number { return (db.prepare("SELECT COUNT(*) AS c FROM users").get() as { c: number }).c; }