import { createHmac, createHash, randomBytes, timingSafeEqual } from 'node:crypto'; import type { NextFunction, Request, Response } from 'express'; import { config } from './config.ts'; /** Comparaison à temps constant, tolérante aux longueurs différentes. */ export function safeEqual(a: string, b: string): boolean { const bufA = Buffer.from(a, 'utf8'); const bufB = Buffer.from(b, 'utf8'); if (bufA.length !== bufB.length) { // On compare quand même pour ne pas court-circuiter sur la longueur. timingSafeEqual(bufA, bufA); return false; } return timingSafeEqual(bufA, bufB); } export function hashToken(token: string): string { return createHash('sha256').update(token).digest('hex'); } export function generateToken(): string { return randomBytes(32).toString('base64url'); } // --- Sessions du dashboard : jeton `.` sans stockage serveur --- function sign(payload: string): string { return createHmac('sha256', config.sessionSecret).update(payload).digest('base64url'); } export function issueSession(): { token: string; expiresAt: number } { const expiresAt = Date.now() + config.sessionTtlMs; const payload = String(expiresAt); return { token: `${payload}.${sign(payload)}`, expiresAt }; } export function verifySession(token: string | undefined | null): boolean { if (!token) return false; const dot = token.lastIndexOf('.'); if (dot <= 0) return false; const payload = token.slice(0, dot); const signature = token.slice(dot + 1); if (!safeEqual(signature, sign(payload))) return false; const expiresAt = Number.parseInt(payload, 10); return Number.isFinite(expiresAt) && expiresAt > Date.now(); } export function extractBearer(header: string | undefined): string | null { if (!header) return null; const [scheme, value] = header.split(' '); if (!value || scheme?.toLowerCase() !== 'bearer') return null; return value.trim(); } /** Middleware Express protégeant les routes /api (hors login). */ export function requireSession(req: Request, res: Response, next: NextFunction): void { const token = extractBearer(req.headers.authorization) ?? (req.query.token as string | undefined); if (!verifySession(token)) { res.status(401).json({ error: 'Non authentifié' }); return; } next(); }