Repository Public MIT
1/** Client HTTP de l'authentification (courriel + mot de passe + code de sécurité). */23export interface AuthUser {4 id: string;5 displayName: string;6 email: string | null;7 emailVerified: boolean;8 createdAt: string;9 lastLoginAt: string | null;10 legacy: boolean;11}1213export type AuthResponse =14 | { status: "ok"; user: AuthUser }15 | { status: "verification_required"; email: string; maskedEmail: string; expiresMin: number }16 | { status: "mfa_required"; maskedEmail: string; expiresMin: number }17 | { status: "sent"; maskedEmail: string; expiresMin: number };1819export class ApiError extends Error {20 constructor(message: string, public status: number, public code?: string, public retryAfterSec?: number) {21 super(message);22 this.name = "ApiError";23 }24}2526async function call<T>(method: string, url: string, body?: unknown): Promise<T> {27 const res = await fetch(url, {28 method,29 headers: body ? { "Content-Type": "application/json" } : undefined,30 body: body ? JSON.stringify(body) : undefined,31 credentials: "include",32 });33 let data: Record<string, unknown> = {};34 try { data = await res.json(); } catch { /* corps vide */ }35 if (!res.ok) {36 const retry = Number(res.headers.get("Retry-After") || 0) || undefined;37 throw new ApiError((data.error as string) || `Erreur ${res.status}`, res.status, data.code as string | undefined, retry);38 }39 return data as T;40}4142export const authApi = {43 register: (b: { name: string; email: string; password: string }) => call<AuthResponse>("POST", "/api/auth/register", b),44 verifyEmail: (b: { email: string; code: string; trustDevice?: boolean }) => call<AuthResponse>("POST", "/api/auth/verify-email", b),45 login: (b: { email: string; password: string }) => call<AuthResponse>("POST", "/api/auth/login", b),46 mfa: (b: { code: string; trustDevice?: boolean }) => call<AuthResponse>("POST", "/api/auth/mfa", b),47 resend: (b: { email?: string; purpose: "verify_email" | "login_mfa" | "reset_password" }) => call<AuthResponse>("POST", "/api/auth/resend-code", b),48 forgot: (b: { email: string }) => call<AuthResponse>("POST", "/api/auth/forgot", b),49 reset: (b: { email: string; code: string; password: string }) => call<AuthResponse>("POST", "/api/auth/reset", b),50 loginToken: (b: { token: string }) => call<AuthResponse>("POST", "/api/auth/login-token", b),51 linkEmail: (b: { email: string; password: string }) => call<AuthResponse>("POST", "/api/auth/link-email", b),52 logout: () => call<{ success: boolean }>("POST", "/api/auth/logout"),53 me: () => call<{ user: AuthUser | null }>("GET", "/api/auth/me"),54 account: () => call<AccountData>("GET", "/api/auth/account"),55 updateProfile: (b: { name: string }) => call<{ user: AuthUser }>("PATCH", "/api/auth/profile", b),56 changePassword: (b: { currentPassword: string; newPassword: string }) => call<{ status: "ok" }>("POST", "/api/auth/change-password", b),57 revokeDevice: (id: string) => call<{ status: "ok" }>("DELETE", `/api/auth/devices/${id}`),58 revokeAllDevices: () => call<{ status: "ok" }>("POST", "/api/auth/devices/revoke-all"),59};6061export interface AccountDevice {62 id: string;63 label: string;64 ip: string | null;65 createdAt: string;66 lastUsedAt: string;67 expiresAt: string;68 current: boolean;69}7071export interface AccountEvent {72 id: string;73 type: string;74 ip: string | null;75 device: string;76 createdAt: string;77 meta: Record<string, unknown> | null;78}7980export interface AccountData {81 user: AuthUser;82 devices: AccountDevice[];83 events: AccountEvent[];84}8586/** Force d'un mot de passe, 0 → 4. */87export function passwordStrength(pw: string): { score: number; label: string } {88 let score = 0;89 if (pw.length >= 8) score++;90 if (pw.length >= 12) score++;91 if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;92 if (/\d/.test(pw) && /[^A-Za-z0-9]/.test(pw)) score++;93 if (pw.length > 0 && (!/[a-zA-Z]/.test(pw) || !/\d/.test(pw))) score = Math.min(score, 1);94 const label = ["Trop court", "Faible", "Correct", "Bon", "Excellent"][score] ?? "";95 return { score, label };96}9798export const EVENT_LABELS: Record<string, string> = {99 login: "Connexion",100 login_failed: "Échec de connexion",101 logout: "Déconnexion",102 register: "Inscription",103 register_duplicate: "Tentative d'inscription (courriel déjà utilisé)",104 email_verified: "Courriel vérifié",105 device_trusted: "Appareil marqué fiable",106 device_revoked: "Appareil révoqué",107 devices_revoked_all: "Tous les appareils révoqués",108 password_changed: "Mot de passe modifié",109 password_change_failed: "Échec de changement de mot de passe",110 password_reset_requested: "Réinitialisation demandée",111 password_reset: "Mot de passe réinitialisé",112 profile_updated: "Profil mis à jour",113 email_link_requested: "Liaison d'un courriel",114 "code_sent:verify_email": "Code de vérification envoyé",115 "code_sent:login_mfa": "Code de sécurité envoyé",116 "code_sent:reset_password": "Code de réinitialisation envoyé",117 "code_failed:verify_email": "Code de vérification incorrect",118 "code_failed:login_mfa": "Code de sécurité incorrect",119 "code_failed:reset_password": "Code de réinitialisation incorrect",120};121