import type Database from 'better-sqlite3'; import bcrypt from 'bcryptjs'; import { randomBytes } from 'node:crypto'; export const SESSION_COOKIE = 'session'; const SESSION_DAYS = 7; export type Role = 'admin' | 'superuser' | 'user'; export function hashPassword(plain: string): string { return bcrypt.hashSync(plain, 12); } export function verifyPassword(plain: string, hash: string): boolean { return bcrypt.compareSync(plain, hash); } export function createUser(db: Database.Database, username: string, password: string, role: Role = 'admin'): number { const res = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)') .run(username, hashPassword(password), role); return Number(res.lastInsertRowid); } export function login(db: Database.Database, username: string, password: string): string | null { const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username) as | { id: number; password_hash: string } | undefined; if (!user || !verifyPassword(password, user.password_hash)) return null; const token = randomBytes(32).toString('hex'); db.prepare( `INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, datetime('now', '+${SESSION_DAYS} days'))` ).run(token, user.id); return token; } export function getSessionUser(db: Database.Database, token: string): { id: number; username: string; role: string } | null { const row = db.prepare( `SELECT s.token, s.expires_at, u.id, u.username, u.role FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token = ?` ).get(token) as { token: string; expires_at: string; id: number; username: string; role: string } | undefined; if (!row) return null; if (row.expires_at <= new Date().toISOString().slice(0, 19).replace('T', ' ')) { db.prepare('DELETE FROM sessions WHERE token = ?').run(token); return null; } return { id: row.id, username: row.username, role: row.role }; } export function logout(db: Database.Database, token: string): void { db.prepare('DELETE FROM sessions WHERE token = ?').run(token); } // Autorizzazione per prefisso di rotta. L'admin passa sempre; per gli altri, la prima regola // che matcha decide; se nessuna regola matcha la rotta è "blog/upload/logout" → consentita a // qualsiasi loggato (il middleware protegge solo /admin e /api/admin, quindi qui arrivano solo // utenti già autenticati). const RULES: [RegExp, Role[]][] = [ [/^\/admin\/users(\/|$)/, ['admin']], [/^\/api\/admin\/users(\/|$)/, ['admin']], [/^\/admin\/content(\/|$)/, ['admin', 'superuser']], [/^\/api\/admin\/content(\/|$)/, ['admin', 'superuser']], ]; export function canAccessAdminPath(role: string, pathname: string): boolean { if (role === 'admin') return true; for (const [re, roles] of RULES) { if (re.test(pathname)) return roles.includes(role as Role); } return true; // blog, upload, logout, showtags: tutti i loggati } export function landingFor(role: string): string { if (role === 'superuser') return '/admin/content'; return '/admin'; }