fix(api,web): corrections auth — gestion d'erreur login, messages explicites

- web: auth/errors (+ tests), LoginPage, client API
- api: auth controller/service

Refs: #14
This commit is contained in:
Git Agent
2026-08-23 11:10:08 +02:00
parent 8f7555cfe4
commit 0af88a63ca
6 changed files with 74 additions and 8 deletions

View File

@ -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<T>(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<T>(path: string, options: RequestOptions = {}): Promise<T> {
try {
const response = await fetch(`${API_BASE}${path}`, {
@ -50,7 +72,7 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
window.dispatchEvent(new CustomEvent("readabook:session-expired"));
}
const detail = await response.text();
throw new Error(detail || `${response.status} ${response.statusText}`);
throw new ApiHttpError(response.status, apiErrorMessage(detail, `${response.status} ${response.statusText}`));
}
return (await response.json()) as T;

View File

@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { ApiHttpError } from "../api/client";
import { loginErrorMessage } from "./errors";
describe("login error messages", () => {
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.");
});
});

View File

@ -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.";
}

View File

@ -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<
<KeyRound size={24} />
<h2>Entrer dans le cabinet</h2>
<ErrorRibbon message={error} />
{status?.initialAdminPasswordIsDefault && (
{status?.hasUsers && (
<div className="initial-admin-box">
<ShieldAlert size={18} />
<div>
<strong>Acces admin initial</strong>
<span>{status.initialAdminEmail}</span>
<code>{DEFAULT_INITIAL_PASSWORD}</code>
<small>Change ces identifiants dans Mon compte &gt; Securite apres connexion.</small>
{status.initialAdminPasswordIsDefault ? (
<>
<code>{DEFAULT_INITIAL_PASSWORD}</code>
<small>Mot de passe par defaut atteste par le serveur. Change-le dans Mon compte &gt; Securite.</small>
</>
) : (
<small>Utilise le mot de passe configure au demarrage ou deja modifie dans le compte.</small>
)}
</div>
</div>
)}