import { createHash, randomBytes } from "crypto"; import { getDb, type ExpertBeoordeling, type Project } from "./db"; // Module 4: experts zijn gebruikers (users.role = 'expert') die per Rikx ronde projecten beoordelen. export type Expert = { id: number; name: string; email: string; actief: boolean; // heeft een wachtwoord ingesteld via de uitnodiging rondeIds: number[]; aantalBeoordelingen: number; created_at: string; }; const INVITE_DAGEN = 7; export function listExperts(): Expert[] { const db = getDb(); const rows = db .prepare( `SELECT u.id, u.name, u.email, u.password_hash, u.created_at, (SELECT COUNT(*) FROM expert_beoordelingen b WHERE b.expert_id = u.id) AS aantal FROM users u WHERE u.role = 'expert' ORDER BY u.name COLLATE NOCASE` ) .all() as { id: number; name: string; email: string; password_hash: string; created_at: string; aantal: number }[]; const rondes = db.prepare("SELECT expert_id, ronde_id FROM expert_rondes").all() as { expert_id: number; ronde_id: number; }[]; return rows.map((r) => ({ id: r.id, name: r.name, email: r.email, actief: r.password_hash.includes(":"), rondeIds: rondes.filter((x) => x.expert_id === r.id).map((x) => x.ronde_id), aantalBeoordelingen: r.aantal, created_at: r.created_at, })); } export function getExpert(id: number): Expert | undefined { return listExperts().find((e) => e.id === id); } export function getExpertRondeIds(expertId: number): number[] { return ( getDb().prepare("SELECT ronde_id FROM expert_rondes WHERE expert_id = ?").all(expertId) as { ronde_id: number }[] ).map((r) => r.ronde_id); } export function setExpertRondes(expertId: number, rondeIds: number[]) { const db = getDb(); db.prepare("DELETE FROM expert_rondes WHERE expert_id = ?").run(expertId); const insert = db.prepare("INSERT OR IGNORE INTO expert_rondes (expert_id, ronde_id) VALUES (?, ?)"); for (const id of rondeIds) insert.run(expertId, id); } // Koppeling vanaf de kant van de ronde (pagina Rikx rondes); alleen bestaande experts tellen mee export function setRondeExperts(rondeId: number, expertIds: number[]) { const db = getDb(); const experts = new Set( (db.prepare("SELECT id FROM users WHERE role = 'expert'").all() as { id: number }[]).map((u) => u.id) ); db.prepare("DELETE FROM expert_rondes WHERE ronde_id = ?").run(rondeId); const insert = db.prepare("INSERT OR IGNORE INTO expert_rondes (expert_id, ronde_id) VALUES (?, ?)"); for (const id of expertIds) if (experts.has(id)) insert.run(id, rondeId); } // Projecten die een expert ziet: status Waardebepaling, in een van zijn rondes export function listProjectenVoorExpert(expertId: number): (Project & { ronde_label: string })[] { const rondeIds = getExpertRondeIds(expertId); if (rondeIds.length === 0) return []; return ( getDb() .prepare( `SELECT p.*, r.label AS ronde_label FROM projects p JOIN rikx_rondes r ON r.id = p.ronde_id WHERE p.status = 'waardebepaling' AND p.ronde_id IN (${rondeIds.map(() => "?").join(",")}) ORDER BY r.label COLLATE NOCASE, p.name COLLATE NOCASE` ) .all(...rondeIds) as (Project & { ronde_label: string })[] ).map((p) => ({ ...p })); } export function magBeoordelen(expertId: number, projectId: number): boolean { return listProjectenVoorExpert(expertId).some((p) => p.id === projectId); } // Beoordelingen van alle experts voor de gegeven projecten, met de naam van de expert export function listBeoordelingen(projectIds: number[]): (ExpertBeoordeling & { expert_name: string })[] { if (projectIds.length === 0) return []; return ( getDb() .prepare( `SELECT b.*, u.name AS expert_name FROM expert_beoordelingen b JOIN users u ON u.id = b.expert_id WHERE b.project_id IN (${projectIds.map(() => "?").join(",")}) ORDER BY u.name COLLATE NOCASE` ) .all(...projectIds) as (ExpertBeoordeling & { expert_name: string })[] ).map((b) => ({ ...b })); } const hashToken = (token: string) => createHash("sha256").update(token).digest("hex"); // Maakt een nieuwe uitnodiging; eerdere, ongebruikte uitnodigingen van deze gebruiker vervallen. export function createInvite(userId: number): string { const db = getDb(); db.prepare("DELETE FROM expert_uitnodigingen WHERE user_id = ? AND used_at IS NULL").run(userId); const token = randomBytes(32).toString("base64url"); db.prepare( `INSERT INTO expert_uitnodigingen (user_id, token_hash, expires_at) VALUES (?, ?, datetime('now', '+${INVITE_DAGEN} days'))` ).run(userId, hashToken(token)); return token; } // Geldige (niet verlopen, niet gebruikte) uitnodiging bij een token, met de bijbehorende expert export function findInvite(token: string) { return getDb() .prepare( `SELECT i.id, i.user_id, u.name, u.email FROM expert_uitnodigingen i JOIN users u ON u.id = i.user_id WHERE i.token_hash = ? AND i.used_at IS NULL AND i.expires_at > datetime('now') AND u.role = 'expert'` ) .get(hashToken(token)) as { id: number; user_id: number; name: string; email: string } | undefined; } export function markInviteUsed(inviteId: number) { getDb().prepare("UPDATE expert_uitnodigingen SET used_at = datetime('now') WHERE id = ?").run(inviteId); } export const UITNODIGING_GELDIG_DAGEN = INVITE_DAGEN; export const MIN_WACHTWOORD_LENGTE = 8;