merge: feat/14-auth-first-run dans develop (auth + first-run)
This commit is contained in:
@ -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 > 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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user