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:
@ -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;
|
||||
|
||||
15
apps/web/src/auth/errors.test.ts
Normal file
15
apps/web/src/auth/errors.test.ts
Normal 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.");
|
||||
});
|
||||
});
|
||||
11
apps/web/src/auth/errors.ts
Normal file
11
apps/web/src/auth/errors.ts
Normal 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.";
|
||||
}
|
||||
@ -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 > 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 > Securite.</small>
|
||||
</>
|
||||
) : (
|
||||
<small>Utilise le mot de passe configure au demarrage ou deja modifie dans le compte.</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user