// MFA forte pour les rôles élevés : TOTP (RFC 6238) via otpauth ; secret chiffré en base ; vérification récente exigée (30 min) pour toute action admin. import * as OTPAuth from "otpauth"; import { eq } from "drizzle-orm"; import { db } from "@/db/client"; import { users, sessions } from "@/db/schema"; import { encrypt, decrypt } from "@/lib/auth/crypto"; import { AuthError, type SessionUser } from "@/lib/auth/session"; import { audit } from "@/lib/auth/audit"; const MFA_WINDOW_MS = 30 * 60000; export async function totpStatus(userId: string): Promise { const [u] = await db().select({ s: users.totpSecretEncrypted }).from(users).where(eq(users.id, userId)); return !!u?.s; } export function newSecret(email: string) { const secret = new OTPAuth.Secret({ size: 20 }); const totp = new OTPAuth.TOTP({ issuer: "Toveli", label: email, algorithm: "SHA1", digits: 6, period: 30, secret }); return { base32: secret.base32, uri: totp.toString() }; } export async function enroll(userId: string, base32: string, code: string): Promise { const totp = new OTPAuth.TOTP({ issuer: "Toveli", digits: 6, period: 30, secret: OTPAuth.Secret.fromBase32(base32) }); if (totp.validate({ token: code, window: 1 }) === null) return false; await db().update(users).set({ totpSecretEncrypted: encrypt(base32), updatedAt: new Date() }).where(eq(users.id, userId)); await audit("mfa.enroll", { actorId: userId }); return true; } export async function verify(user: SessionUser, code: string): Promise { const [u] = await db().select({ s: users.totpSecretEncrypted }).from(users).where(eq(users.id, user.id)); if (!u?.s) return false; const totp = new OTPAuth.TOTP({ issuer: "Toveli", digits: 6, period: 30, secret: OTPAuth.Secret.fromBase32(decrypt(u.s)) }); const ok = totp.validate({ token: code, window: 1 }) !== null; if (ok) await db().update(sessions).set({ mfaAt: new Date() }).where(eq(sessions.id, user.sessionId)); await audit(ok ? "mfa.ok" : "mfa.fail", { actorId: user.id }); return ok; } /** Garde des actions admin : rôle + TOTP vérifié dans la fenêtre. */ export function requireFreshMfa(u: SessionUser) { if (!u.mfaAt || Date.now() - u.mfaAt.getTime() > MFA_WINDOW_MS) throw new AuthError(403, "Vérification TOTP requise"); } export function isFreshMfa(mfaAt: Date | null | undefined): boolean { return !!mfaAt && Date.now() - mfaAt.getTime() <= MFA_WINDOW_MS; }