Personnaliser la connexion
Envelopper les pages d'authentification dans une mise en page maison, avec le patron serveur et client.
Par défaut, une seule route catch-all expose toutes les pages d'authentification :
// app/auth/[...auth]/page.js
export { default, generateMetadata } from '@lrvo/core/features/auth/server';Cette page décrit comment remplacer ce câblage par des routes individuelles, pour envelopper chaque page dans une mise en page existante.
Le patron
Chaque page suit la même structure : un composant serveur qui charge la session et passe les actions, et un wrapper client qui gère la navigation.
- Composants :
@lrvo/core/features/auth/pages - Actions :
@lrvo/core/features/auth/actions
La séparation est nécessaire parce que les server actions ne peuvent pas être importées depuis un composant client. Le composant serveur les reçoit et les transmet en props.
Composants disponibles
| Page | Composant | Props principales |
|---|---|---|
| Première configuration | FirstRunPage | onSubmit |
| Connexion | LoginPage | onSubmit, onResendVerification, onSetSessionCookie, onNavigate, redirectAfterLogin, currentUser, registrationEnabled |
| Inscription | RegisterPage | onSubmit, onNavigate, currentUser, registrationEnabled |
| Mot de passe oublié | ForgotPasswordPage | onSubmit, onNavigate, currentUser |
| Réinitialisation | ResetPasswordPage | onSubmit, onNavigate, email, token |
| Confirmation d'adresse | ConfirmEmailPage | onSubmit, onNavigate, email, token |
| Configuration de compte | SetupAccountPage | onSubmit, onNavigate, email, token |
| Déconnexion | LogoutPage | onLogout, onSetSessionCookie |
onNavigate reçoit une valeur parmi 'login', 'register', 'forgot', 'reset'.
onResendVerification reçoit resendVerificationAction. Sans elle, un compte non vérifié voit le message de refus mais pas le bouton « Renvoyer le lien de vérification ». Voir Vérification du courriel.
redirectAfterLogin est la destination d'un utilisateur connecté, / par défaut. LoginPage y navigue une seule fois, par router.replace : à la soumission réussie, ou dès que currentUser est renseigné, selon ce qui arrive en premier. Les deux se produisent pour une même connexion, parce que le cookie posé par loginAction fait rafraîchir la route par Next.js. Ne pas ajouter de redirection dans le wrapper : elle rechargerait la destination une seconde fois, sous les yeux de l'utilisateur.
onSetSessionCookie ne sert qu'à un onSubmit maison qui retourne un sessionToken, après une authentification externe. loginAction pose le cookie elle-même et n'en retourne pas.
registrationEnabled vaut true par défaut. Pour respecter LRVO_AUTH_REGISTRATION, la lire côté serveur avec isRegistrationOpen() de @lrvo/core/features/auth et la transmettre à LoginPage et RegisterPage. Le lien vers l'inscription disparaît alors de la connexion, et la page d'inscription affiche un message de fermeture au lieu du formulaire. Voir Fermer les inscriptions.
Exemple : page de connexion
Serveur
// app/auth/login/page.js
import { getSession, loginAction, resendVerificationAction, setSessionCookie } from '@lrvo/core/features/auth/actions';
import { LoginPageWrapper } from './LoginPageWrapper';
import MonLayout from '@/components/MonLayout';
export default async function LoginRoute() {
const session = await getSession();
return (
<MonLayout>
<LoginPageWrapper
loginAction={loginAction}
resendVerificationAction={resendVerificationAction}
setSessionCookie={setSessionCookie}
currentUser={session?.user ?? null}
/>
</MonLayout>
);
}Client
// app/auth/login/LoginPageWrapper.js
'use client';
import { useRouter } from 'next/navigation';
import { LoginPage } from '@lrvo/core/features/auth/pages';
export function LoginPageWrapper({ loginAction, resendVerificationAction, setSessionCookie, currentUser }) {
const router = useRouter();
return (
<LoginPage
onSubmit={loginAction}
onResendVerification={resendVerificationAction}
onSetSessionCookie={setSessionCookie}
onNavigate={(page) => router.push(`/auth/${page}`)}
redirectAfterLogin="/"
currentUser={currentUser}
/>
);
}Les autres pages
Même patron, avec ces différences :
RegisterPage: passerregisterActioncommeonSubmit.ForgotPasswordPage: passerforgotPasswordAction.ResetPasswordPage,ConfirmEmailPage,SetupAccountPage: lireemailettokendanssearchParamset les passer en props.LogoutPage: utiliseronLogoutau lieu deonSubmit, sansonNavigate.
// app/auth/reset/page.js
export default async function ResetRoute({ searchParams }) {
const { email, token } = await searchParams;
return (
<MonLayout>
<ResetPasswordWrapper resetAction={resetPasswordAction} email={email} token={token} />
</MonLayout>
);
}La première configuration
La route catch-all remplace toutes les pages par FirstRunPage tant qu'aucun compte n'existe. Des routes individuelles doivent faire ce test elles-mêmes, sinon une installation neuve n'a aucun formulaire pour créer son administrateur :
// app/auth/login/page.js
import { needsFirstRun } from '@lrvo/core/features/auth';
import { firstRunAction } from '@lrvo/core/features/auth/actions';
import { FirstRunWrapper } from './FirstRunWrapper';
export default async function LoginRoute() {
if (await needsFirstRun()) {
return <MonLayout><FirstRunWrapper firstRunAction={firstRunAction} /></MonLayout>;
}
// … page de connexion habituelle
}FirstRunWrapper est un composant client qui rend <FirstRunPage onSubmit={firstRunAction} />. firstRunAction redirige elle-même vers /admin après la création, il n'y a rien à faire côté client.
Protéger une route
import { getSession } from '@lrvo/core/features/auth/actions';
import { redirect } from 'next/navigation';
export default async function PageProtegee() {
const session = await getSession();
if (!session?.user) redirect('/auth/login');
// …
}Pour une page d'admin, utiliser protectAdmin() de @lrvo/core/features/admin/protect, qui vérifie en plus la permission admin.access.
Ce qui reste géré par le core
Remplacer la mise en page ne désactive rien de la sécurité. La limitation de débit par IP, les champs anti-bot, le cookie HttpOnly et la validation du token contre la base restent appliqués par les server actions.
Il en va de même pour la fermeture des inscriptions : registerAction refuse la création quand LRVO_AUTH_REGISTRATION=false, que la prop registrationEnabled soit passée ou non. Oublier la prop ne rouvre donc rien, cela se traduit par un formulaire visible dont la soumission est refusée.
En particulier, ne pas réimplémenter setSessionCookie : il valide le token en base avant d'écrire le cookie, ce qui empêche qu'un token arbitraire soit accepté.
Le titre d'onglet
Le titre d'onglet, lui, ne suit pas : il vient du generateMetadata de la route catch-all, que les routes individuelles remplacent. Déclarer un metadata dans chacune, faute de quoi l'onglet affiche le nom de domaine.
// app/auth/login/page.js
export const metadata = { title: 'Connexion' };Revenir au comportement par défaut
Supprimer les routes individuelles et rétablir la route catch-all. Les deux ne peuvent pas coexister sur les mêmes chemins.