From 0af88a63cab3ee49be5946776e19eb93b9f22eb6 Mon Sep 17 00:00:00 2001 From: Git Agent Date: Sun, 23 Aug 2026 11:10:08 +0200 Subject: [PATCH] =?UTF-8?q?fix(api,web):=20corrections=20auth=20=E2=80=94?= =?UTF-8?q?=20gestion=20d'erreur=20login,=20messages=20explicites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web: auth/errors (+ tests), LoginPage, client API - api: auth controller/service Refs: #14 --- apps/api/src/auth/auth.controller.ts | 2 +- apps/api/src/auth/auth.service.ts | 15 +++++++++++++-- apps/web/src/api/client.ts | 24 +++++++++++++++++++++++- apps/web/src/auth/errors.test.ts | 15 +++++++++++++++ apps/web/src/auth/errors.ts | 11 +++++++++++ apps/web/src/pages/LoginPage.tsx | 15 +++++++++++---- 6 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/auth/errors.test.ts create mode 100644 apps/web/src/auth/errors.ts diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 9ba269c..a8da930 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -19,7 +19,7 @@ export class AuthController { constructor(private readonly auth: AuthService) {} @Get("status") - status() { + async status() { return this.auth.status(); } diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 7fbf4a9..d6a6f8d 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -33,12 +33,23 @@ export class AuthService implements OnModuleInit { return this.createUser({ ...input, role: "admin" }); } - status() { + async status() { const existing = this.database.db.select({ id: users.id }).from(users).limit(1).get(); + const initialAdmin = this.database.db + .select({ passwordHash: users.passwordHash, role: users.role }) + .from(users) + .where(eq(users.email, this.database.config.initialAdminEmail.toLowerCase())) + .get(); + const initialAdminPasswordIsDefault = + Boolean(initialAdmin) && + initialAdmin?.role === "admin" && + this.database.config.initialAdminPasswordIsDefault && + (await argon2.verify(initialAdmin.passwordHash, this.database.config.initialAdminPassword)); + return { hasUsers: Boolean(existing), initialAdminEmail: this.database.config.initialAdminEmail.toLowerCase(), - initialAdminPasswordIsDefault: this.database.config.initialAdminPasswordIsDefault + initialAdminPasswordIsDefault }; } diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 6f9190e..93825f2 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -30,10 +30,32 @@ export class ApiFallbackError extends Error { } } +export class ApiHttpError extends Error { + constructor( + public readonly status: number, + message: string + ) { + super(message); + } +} + export function getApiFallback(error: unknown): T | undefined { return error instanceof ApiFallbackError ? (error.fallback as T) : undefined; } +function apiErrorMessage(detail: string, fallback: string): string { + if (!detail) return fallback; + try { + const parsed = JSON.parse(detail) as { message?: unknown; error?: unknown }; + if (typeof parsed.message === "string") return parsed.message; + if (Array.isArray(parsed.message)) return parsed.message.join(", "); + if (typeof parsed.error === "string") return parsed.error; + } catch { + return detail; + } + return fallback; +} + async function request(path: string, options: RequestOptions = {}): Promise { try { const response = await fetch(`${API_BASE}${path}`, { @@ -50,7 +72,7 @@ async function request(path: string, options: RequestOptions = {}): Promise { + it("maps invalid credentials", () => { + expect(loginErrorMessage(new ApiHttpError(401, "Invalid credentials"))).toBe("Identifiants invalides."); + expect(loginErrorMessage(new ApiHttpError(403, "Forbidden"))).toBe("Identifiants invalides."); + }); + + it("maps server and network errors", () => { + expect(loginErrorMessage(new ApiHttpError(500, "Internal error"))).toBe("Serveur d'authentification indisponible."); + expect(loginErrorMessage(new TypeError("fetch failed"))).toBe("Connexion au serveur impossible."); + }); +}); diff --git a/apps/web/src/auth/errors.ts b/apps/web/src/auth/errors.ts new file mode 100644 index 0000000..22089d7 --- /dev/null +++ b/apps/web/src/auth/errors.ts @@ -0,0 +1,11 @@ +import { ApiHttpError } from "../api/client"; + +export function loginErrorMessage(error: unknown): string { + if (error instanceof ApiHttpError) { + if (error.status === 401 || error.status === 403) return "Identifiants invalides."; + if (error.status >= 500) return "Serveur d'authentification indisponible."; + return "Connexion impossible."; + } + if (error instanceof TypeError) return "Connexion au serveur impossible."; + return "Connexion impossible."; +} diff --git a/apps/web/src/pages/LoginPage.tsx b/apps/web/src/pages/LoginPage.tsx index 65608bf..98de627 100644 --- a/apps/web/src/pages/LoginPage.tsx +++ b/apps/web/src/pages/LoginPage.tsx @@ -2,6 +2,7 @@ import { FormEvent, useEffect, useState } from "react"; import { KeyRound, LogIn, ShieldAlert } from "lucide-react"; import type { AuthStatusDto } from "@readabook/shared"; import { api } from "../api/client"; +import { loginErrorMessage } from "../auth/errors"; import { navigate } from "../router"; import { ErrorRibbon, Panel } from "../components/ui"; @@ -38,7 +39,7 @@ export function LoginPage({ onSessionChange }: { onSessionChange: () => Promise< await onSessionChange(); navigate("/home"); } catch (loginError) { - setError(loginError instanceof Error ? loginError.message : "Connexion impossible"); + setError(loginErrorMessage(loginError)); } } @@ -53,14 +54,20 @@ export function LoginPage({ onSessionChange }: { onSessionChange: () => Promise<

Entrer dans le cabinet

- {status?.initialAdminPasswordIsDefault && ( + {status?.hasUsers && (
Acces admin initial {status.initialAdminEmail} - {DEFAULT_INITIAL_PASSWORD} - Change ces identifiants dans Mon compte > Securite apres connexion. + {status.initialAdminPasswordIsDefault ? ( + <> + {DEFAULT_INITIAL_PASSWORD} + Mot de passe par defaut atteste par le serveur. Change-le dans Mon compte > Securite. + + ) : ( + Utilise le mot de passe configure au demarrage ou deja modifie dans le compte. + )}
)}