merge: feat/14-auth-first-run dans develop (auth + first-run)

This commit is contained in:
Git Agent
2026-08-23 11:13:09 +02:00
14 changed files with 394 additions and 18 deletions

View File

@ -1,14 +1,36 @@
import { FormEvent, useState } from "react";
import { KeyRound, LogIn } from "lucide-react";
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";
const DEFAULT_INITIAL_PASSWORD = "readabook-admin-change-me";
export function LoginPage({ onSessionChange }: { onSessionChange: () => Promise<void> }) {
const [email, setEmail] = useState("admin@readabook.local");
const [status, setStatus] = useState<AuthStatusDto | null>(null);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string>();
useEffect(() => {
let alive = true;
api
.authStatus()
.then((nextStatus) => {
if (!alive) return;
setStatus(nextStatus);
setEmail((current) => current || nextStatus.initialAdminEmail);
})
.catch(() => {
if (alive) setError("Statut d'authentification indisponible.");
});
return () => {
alive = false;
};
}, []);
async function submit(event: FormEvent) {
event.preventDefault();
setError(undefined);
@ -17,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));
}
}
@ -32,6 +54,23 @@ export function LoginPage({ onSessionChange }: { onSessionChange: () => Promise<
<KeyRound size={24} />
<h2>Entrer dans le cabinet</h2>
<ErrorRibbon message={error} />
{status?.hasUsers && (
<div className="initial-admin-box">
<ShieldAlert size={18} />
<div>
<strong>Acces admin initial</strong>
<span>{status.initialAdminEmail}</span>
{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>
)}
<form onSubmit={submit} className="stack-form">
<label>
Email
@ -46,9 +85,11 @@ export function LoginPage({ onSessionChange }: { onSessionChange: () => Promise<
Se connecter
</button>
</form>
<button className="ghost-button full-width" onClick={() => navigate("/setup/admin")}>
Initialiser le premier admin
</button>
{status && !status.hasUsers && (
<button className="ghost-button full-width" onClick={() => navigate("/setup/admin")}>
Initialiser le premier admin
</button>
)}
</Panel>
</div>
);

View File

@ -1,19 +1,47 @@
import { LogOut, UserRound } from "lucide-react";
import { FormEvent, useState } from "react";
import { KeyRound, LogOut, UserRound } from "lucide-react";
import type { Session } from "../api/types";
import { api } from "../api/client";
import { Panel } from "../components/ui";
import { ErrorRibbon, Panel } from "../components/ui";
import { navigate } from "../router";
export function ProfilePage({ session, onSessionChange }: { session: Session; onSessionChange: () => Promise<void> }) {
const [email, setEmail] = useState(session.user?.email ?? "");
const [name, setName] = useState(session.user?.name ?? "");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [error, setError] = useState<string>();
const [success, setSuccess] = useState<string>();
async function logout() {
await api.logout();
await onSessionChange();
navigate("/login");
}
async function updateSecurity(event: FormEvent) {
event.preventDefault();
setError(undefined);
setSuccess(undefined);
try {
await api.updateMe({
email: email === session.user?.email ? undefined : email,
name: name || null,
currentPassword,
newPassword: newPassword || undefined
});
setCurrentPassword("");
setNewPassword("");
setSuccess("Identifiants mis a jour.");
await onSessionChange();
} catch (updateError) {
setError(updateError instanceof Error ? updateError.message : "Mise a jour impossible");
}
}
return (
<div className="page-grid">
<Panel className="span-2 profile-panel">
<Panel className="profile-panel">
<UserRound size={28} />
<h1>{session.user?.name ?? "Lecteur invite"}</h1>
<p>{session.user?.email ?? "Session non connectee"}</p>
@ -23,6 +51,36 @@ export function ProfilePage({ session, onSessionChange }: { session: Session; on
Sortir
</button>
</Panel>
<Panel className="span-2">
<div className="section-heading">
<h2>Securite</h2>
<KeyRound size={20} />
</div>
<p className="muted-copy">Change l'email et le mot de passe admin initial des que le cabinet est installe.</p>
<ErrorRibbon message={error} />
{success && <div className="success-ribbon">{success}</div>}
<form className="stack-form" onSubmit={updateSecurity}>
<label>
Email
<input value={email} onChange={(event) => setEmail(event.target.value)} type="email" required />
</label>
<label>
Nom
<input value={name} onChange={(event) => setName(event.target.value)} />
</label>
<label>
Mot de passe actuel
<input value={currentPassword} onChange={(event) => setCurrentPassword(event.target.value)} type="password" required />
</label>
<label>
Nouveau mot de passe
<input value={newPassword} onChange={(event) => setNewPassword(event.target.value)} type="password" minLength={8} />
</label>
<button className="primary-button" type="submit">
Enregistrer
</button>
</form>
</Panel>
</div>
);
}