import crypto from 'crypto'; import { getDb } from '../db/index.js'; import { hashPassword, verifyPassword } from '../lib/password.js'; // Dashboard authentication: email + password accounts with opaque session // tokens. Distinct from the unified API key, which authenticates the /v1 proxy // for apps — this gates the /api/* admin surface for the human operator (#35). const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days export interface SessionUser { userId: number; email: string; } function sha256(s: string): string { return crypto.createHash('sha256').update(s).digest('hex'); } function normalizeEmail(email: string): string { return email.trim().toLowerCase(); } export async function userCount(): Promise { const row = await getDb().get<{ c: number }>('SELECT COUNT(*) AS c FROM users'); return row?.c ?? 0; } /** Create a user. Throws { code: 'email_taken' } if the email already exists. */ export async function createUser(email: string, password: string): Promise { const db = getDb(); const normalized = normalizeEmail(email); const existing = await db.get('SELECT id FROM users WHERE email = ?', [normalized]); if (existing) { const err = new Error('An account with that email already exists') as any; err.code = 'email_taken'; throw err; } const result = await db.run('INSERT INTO users (email, password_hash) VALUES (?, ?)', [normalized, hashPassword(password)]); return { userId: Number(result.lastInsertRowid), email: normalized }; } /** Verify credentials. Returns the user on success, null on failure. */ export async function verifyCredentials(email: string, password: string): Promise { const db = getDb(); const row = await db.get<{ id: number; email: string; password_hash: string }>( 'SELECT id, email, password_hash FROM users WHERE email = ?', [normalizeEmail(email)], ); if (!row) return null; if (!verifyPassword(password, row.password_hash)) return null; return { userId: row.id, email: row.email }; } /** Mint a session and return the raw token (only the hash is persisted). */ export async function createSession(userId: number): Promise { const token = crypto.randomBytes(32).toString('hex'); await getDb().run('INSERT INTO sessions (token_hash, user_id, expires_at_ms) VALUES (?, ?, ?)', [sha256(token), userId, Date.now() + SESSION_TTL_MS]); return token; } /** Resolve a session token to its user, or null if missing/expired. */ export async function validateSession(token: string | undefined | null): Promise { if (!token) return null; const db = getDb(); const row = await db.get<{ user_id: number; expires_at_ms: number; email: string }>(` SELECT s.user_id, s.expires_at_ms, u.email FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token_hash = ? `, [sha256(token)]); if (!row) return null; if (row.expires_at_ms < Date.now()) { await db.run('DELETE FROM sessions WHERE token_hash = ?', [sha256(token)]); return null; } return { userId: row.user_id, email: row.email }; } export async function deleteSession(token: string | undefined | null): Promise { if (!token) return; await getDb().run('DELETE FROM sessions WHERE token_hash = ?', [sha256(token)]); }