SPB Git

Repository Public MIT

auth-api.ts 5.1 KB 121 lines typescript

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
Files main
Menu

Theme