import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import { initDb, query } from "./db.ts";
import type { SessionUser } from "../utils.ts";
const SESSION_COOKIE = "profectus_session";
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30;
export const ADMIN_EMAIL = Deno.env.get("ADMIN_EMAIL")?.toLowerCase() ??
"silen.locatelli@gmx.ch";
export interface UserRecord extends SessionUser {
password_hash: string;
created_at: string;
}
export function hashPassword(password: string): string {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(password, salt, 64).toString("hex");
return `${salt}:${hash}`;
}
export function verifyPassword(password: string, stored: string): boolean {
const [salt, hash] = stored.split(":");
if (!salt || !hash) return false;
const candidate = scryptSync(password, salt, 64);
const expected = Buffer.from(hash, "hex");
return candidate.length === expected.length &&
timingSafeEqual(candidate, expected);
}
function getSessionToken(req: Request): string | null {
const header = req.headers.get("cookie");
if (!header) return null;
for (const part of header.split(";")) {
const idx = part.indexOf("=");
if (idx === -1) continue;
if (part.slice(0, idx).trim() === SESSION_COOKIE) {
return decodeURIComponent(part.slice(idx + 1).trim());
}
}
return null;
}
function cookieAttributes(req: Request, maxAge: number): string {
const secure = new URL(req.url).protocol === "https:";
return `Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${
secure ? "; Secure" : ""
}`;
}
export async function getUserFromRequest(
req: Request,
): Promise<SessionUser | null> {
await initDb();
const token = getSessionToken(req);
if (!token) return null;
const result = await query(
`SELECT u.id, u.email, u.name, u.role
FROM sessions s JOIN users u ON u.id = s.user_id
WHERE s.token = ? AND s.expires_at > datetime('now')`,
[token],
);
const row = result.rows[0];
if (!row) return null;
return {
id: Number(row.id),
email: String(row.email),
name: row.name === null ? null : String(row.name),
role: String(row.role) as SessionUser["role"],
};
}
export async function createSession(userId: number): Promise<string> {
await initDb();
const token = randomBytes(32).toString("hex");
await query(
`INSERT INTO sessions (token, user_id, expires_at)
VALUES (?, ?, datetime('now', '+${SESSION_TTL_SECONDS} seconds'))`,
[token, userId],
);
return token;
}
export function sessionCookie(req: Request, token: string): string {
return `${SESSION_COOKIE}=${encodeURIComponent(token)}; ${
cookieAttributes(req, SESSION_TTL_SECONDS)
}`;
}
export async function destroySession(req: Request): Promise<void> {
const token = getSessionToken(req);
if (token) {
await initDb();
await query("DELETE FROM sessions WHERE token = ?", [token]);
}
}
export function clearSessionCookie(req: Request): string {
return `${SESSION_COOKIE}=; ${cookieAttributes(req, 0)}`;
}
export async function findUserByEmail(
email: string,
): Promise<UserRecord | null> {
await initDb();
const result = await query(
`SELECT id, email, name, password_hash, role, created_at
FROM users WHERE email = ?`,
[email],
);
const row = result.rows[0];
if (!row) return null;
return {
id: Number(row.id),
email: String(row.email),
name: row.name === null ? null : String(row.name),
role: String(row.role) as UserRecord["role"],
password_hash: String(row.password_hash),
created_at: String(row.created_at),
};
}
export async function createUser(
email: string,
name: string | null,
passwordHash: string,
): Promise<UserRecord> {
await initDb();
const role = email.toLowerCase() === ADMIN_EMAIL ? "admin" : "user";
const result = await query(
`INSERT INTO users (email, name, password_hash, role)
VALUES (?, ?, ?, ?)`,
[email, name, passwordHash, role],
);
return {
id: Number(result.lastInsertRowid),
email,
name,
role: role as UserRecord["role"],
password_hash: passwordHash,
created_at: new Date().toISOString(),
};
}
export async function setUserRole(
userId: number,
role: UserRecord["role"],
): Promise<void> {
await query("UPDATE users SET role = ? WHERE id = ?", [role, userId]);
}
export async function deleteUser(userId: number): Promise<void> {
await query("DELETE FROM sessions WHERE user_id = ?", [userId]);
await query("DELETE FROM users WHERE id = ?", [userId]);
}