Authentification
Server actions, routes REST et pages prêtes à l'emploi pour la première configuration, l'inscription, la connexion, les sessions et le profil.
features/auth gère l'authentification de bout en bout : inscription, connexion, sessions, réinitialisation de mot de passe, vérification d'adresse et profil. Il expose des server actions Next.js, des routes REST et des composants de pages.
Règle absolue : toute authentification de site passe par cette feature. Elle s'appuie sur core/users pour les données.
Import
// Server actions — 'use server'
import { getSession, loginAction, logoutAction } from '@lrvo/core/features/auth/actions';
// Composants de pages — client
import { LoginPage, RegisterPage, FirstRunPage } from '@lrvo/core/features/auth/pages';
// Barrel serveur
import {
validateSession, createSession, completeAccountSetup,
sendVerificationEmail, sendPasswordResetEmail, sendPasswordChangedEmail, sendInvitationEmail,
} from '@lrvo/core/features/auth';
// Page racine RSC de la route catch-all
export { default, generateMetadata } from '@lrvo/core/features/auth/server';@lrvo/core/features/auth et .../actions sont serveur. .../pages et .../client sont client.
Pages intégrées
Une seule route catch-all suffit à exposer toutes les pages :
// app/auth/[...auth]/page.js
export { default, generateMetadata } from '@lrvo/core/features/auth/server';| Route | Page |
|---|---|
/auth/login | Connexion |
/auth/register | Inscription |
/auth/forgot | Mot de passe oublié |
/auth/reset | Réinitialisation du mot de passe |
/auth/confirm | Vérification de l'adresse |
/auth/setup | Configuration du compte après invitation admin |
/auth/logout | Déconnexion |
Tant que lrvo_auth_users est vide, la route rend FirstRunPage quel que soit le segment : /auth, /auth/login et la redirection de protectAdmin mènent au même formulaire. Voir Première configuration.
Pour envelopper ces pages dans une mise en page maison, voir Personnaliser la connexion.
Titre d'onglet
La route réexporte aussi generateMetadata, qui pose le <title> de chaque page. Sans cet export, aucune page d'authentification n'a de titre : le gabarit racine n'en déclare pas, et l'onglet affiche le nom de domaine.
Le titre est "<page> — <appName>", où <page> reprend le titre de la carte et <appName> vient de LRVO_NAME.
| Segment | Titre |
|---|---|
login, ou un segment inconnu | Connexion |
register | Créer un compte |
forgot | Mot de passe oublié |
reset | Nouveau mot de passe |
confirm | Vérification du courriel |
setup | Activer le compte |
logout | Déconnexion |
Tant qu'aucun compte n'existe, le titre est « Première configuration » quel que soit le segment, comme la page rendue.
lrvo-doctor --fix remonte une route restée au export { default } seul.
Apparence
Les pages partagent une même carte, AuthCard, interne à features/auth/components/ :
- un en-tête centré, avec un titre de 24 px et une description courte ;
- le formulaire, avec 20 px entre les champs ;
- un pied séparé par une bordure sur toute la largeur, qui porte le lien secondaire : « Pas encore de compte ? Créer un compte », « Déjà un compte ? Se connecter » ou « Retour à la connexion ».
Les champs ne portent pas d'astérisque : tous sont requis, le marquer n'apprend rien. Le champ mot de passe d'une création rappelle les règles tant qu'il est vide, puis PasswordStrengthIndicator prend le relais. Aucun bouton n'affiche le mot de passe en clair.
Les textes suivent la voix de l'interface décrite dans Rédaction : impersonnels, « courriel » et jamais « e-mail », pas de « avec succès ». Les messages renvoyés par les server actions suivent la même règle, puisque les pages les affichent tels quels.
Première configuration
Sur une base sans aucun compte, la route catch-all remplace toutes les pages par un formulaire unique qui crée l'administrateur. AuthPage.server.js le décide avec needsFirstRun(), à chaque rendu : dès que le compte existe, le formulaire disparaît.
Le compte créé :
- reçoit le rôle système
admin, doncadmin.access; - a
email_verified = true, et aucun courriel ne part ; - repart avec une session ouverte, le navigateur étant envoyé vers
/admin.
La création passe par createFirstAdmin() de core/users, qui compte et insère dans une transaction verrouillée. Deux soumissions simultanées ne produisent donc qu'un administrateur.
Tant que la base est vide, la première personne qui atteint /auth devient administrateur. Créer ce compte avant d'exposer le site publiquement.
Vérification du courriel
loginAction refuse un compte dont email_verified est faux. Le refus porte le code email_unverified et n'est émis qu'après un mot de passe correct : sans le mot de passe, un compte non vérifié répond comme un compte inexistant.
const result = await loginAction(formData);
// { success: false, error: "Le courriel de ce compte n'a pas été vérifié.", code: 'email_unverified' }LoginPage affiche alors un bouton « Renvoyer le lien de vérification », branché sur resendVerificationAction par la prop onResendVerification.
Ce qui passe email_verified à vrai :
| Chemin | Fonction |
|---|---|
Lien de /auth/confirm | verifyEmailAction |
| Réinitialisation du mot de passe par lien | resetPassword() |
Invitation acceptée sur /auth/setup | completeAccountSetup() |
| Création par l'admin avec mot de passe | POST /lrvo/api/users |
| Action manuelle d'un admin | PUT /lrvo/api/users/:id avec { email_verified: true }, bouton de la fiche utilisateur |
| Première configuration | createFirstAdmin() |
La garde porte sur la connexion, pas sur la session : une session ouverte avant que la règle existe reste valide jusqu'à son expiration.
Fermer les inscriptions
LRVO_AUTH_REGISTRATION=false ferme les inscriptions publiques. Le drapeau est en opt-out : sans la variable, ou avec n'importe quelle autre valeur, les inscriptions restent ouvertes.
Ce que la fermeture change :
registerActionrefuse la création et retourne{ success: false, error: 'Les inscriptions sont actuellement fermées.' }. Le refus est aussi appliqué dansregister()decore/users, donc un module qui appelle la couche de données directement se heurte au même verrou./auth/registeraffiche un message de fermeture à la place du formulaire, et le lien « Pas encore de compte ? Créer un compte » disparaît de la page de connexion.
Ce que la fermeture ne change pas :
- Le tout premier compte. Tant que
lrvo_auth_usersest vide, le formulaire de première configuration reste disponible, etregister()elle-même laisse passer ce compte et lui donne le rôle admin. Sans cette exception, une installation neuve configurée àfalsen'aurait aucun moyen de créer son administrateur. - La création de comptes par un admin.
POST /lrvo/api/userset le parcours d'invitation vers/auth/setupne passent pas parregister(). C'est la voie prévue pour ajouter des comptes une fois les inscriptions fermées.
isRegistrationOpen()
Exportée par @lrvo/core/features/auth et @lrvo/core/users. Retourne false quand le drapeau est à false et qu'au moins un utilisateur existe. À utiliser pour conditionner sa propre interface :
import { isRegistrationOpen } from '@lrvo/core/features/auth';
const registrationEnabled = await isRegistrationOpen();Les pages LoginPage et RegisterPage acceptent la prop registrationEnabled, que la route catch-all renseigne déjà. En câblage manuel, la prop vaut true par défaut ; l'oublier ne rouvre pas les inscriptions, cela se traduit par un formulaire visible dont la soumission est refusée.
Server actions
Toutes attendent un FormData, sauf getSession, setSessionCookie et refreshSessionCookie.
getSession()
Lit le cookie de session et retourne la session, ou null. Renouvelle le cookie automatiquement si la session a été rafraîchie.
const session = await getSession();
if (!session?.user) redirect('/auth/login');loginAction(formData)
Authentifie et pose un cookie HttpOnly. Applique le rate limit par IP et les vérifications anti-bot.
const result = await loginAction(formData);
// { success: true, user } ou { success: false, error, code? }code vaut 'email_unverified' quand le mot de passe est bon mais le courriel pas vérifié. Voir Vérification du courriel.
registerAction(formData)
Crée un compte et envoie le courriel de vérification. Refuse la création quand les inscriptions sont fermées, voir Fermer les inscriptions.
Un envoi en échec ne transforme pas l'inscription en erreur : le compte existe déjà. L'action journalise l'échec et répond par un succès dont le message invite à redemander le lien depuis la connexion.
| Champ | Rôle |
|---|---|
email | Adresse. |
password | Mot de passe. |
name | Nom d'affichage. |
firstRunAction(formData)
Crée le premier administrateur, pose le cookie de session et redirige vers /admin avec redirect(). La redirection est faite côté serveur : poser le cookie fait rafraîchir la route /auth par Next.js, qui rendrait alors la page de connexion d'un utilisateur connecté et lancerait sa propre navigation, en concurrence avec une redirection côté client. En cas d'échec, l'action retourne { success: false, error }. En cas de succès, Next.js rejette la promesse de l'action avec son erreur de redirection (digest qui commence par NEXT_REDIRECT) avant de naviguer. FirstRunPage la reconnaît comme un succès ; un formulaire maison qui appelle firstRunAction doit faire de même au lieu d'afficher une erreur. Échoue avec « Le compte administrateur existe déjà » dès qu'un compte existe. Anti-bot et rate limit register appliqués. Champs : email, password, name.
resendVerificationAction(formData)
Émet un nouveau lien de vérification si un compte non vérifié correspond à email. La réponse est identique dans tous les cas, échec d'envoi compris, pour ne pas révéler l'existence du compte. Anti-bot et rate limit resend_verification appliqués.
logoutAction()
Invalide la session en base et supprime le cookie.
forgotPasswordAction(formData)
Envoie un lien de réinitialisation si un compte existe. La réponse est identique dans les deux cas, pour ne pas révéler l'existence du compte.
resetPasswordAction(formData)
| Champ | Rôle |
|---|---|
email | Adresse. |
token | Token reçu par courriel. |
newPassword | Nouveau mot de passe. |
verifyEmailAction(formData)
Vérifie le token de confirmation et marque l'adresse comme vérifiée. Champs : email, token.
setupAccountAction(formData)
Vérifie le token d'invitation, crée le compte credential et marque l'adresse comme vérifiée. Appelée depuis /auth/setup après qu'un admin a créé le compte sans mot de passe.
| Champ | Rôle |
|---|---|
email | Adresse. |
token | Token du courriel d'invitation. |
newPassword | Mot de passe choisi. |
confirmPassword | Confirmation. |
setSessionCookie(token)
Valide le token contre la base avant de l'écrire dans le cookie HttpOnly. À utiliser après une authentification externe. La validation évite qu'un token arbitraire soit accepté.
refreshSessionCookie(token)
Revalide le token et prolonge le cookie à 30 jours.
Routes REST
Enregistrées automatiquement sous /lrvo/api. L'authentification est appliquée par le routeur avant chaque handler, voir API.
Toutes les routes d'administration sont déclarées auth: 'admin' et portent une permission granulaire : admin.access ne suffit pas, la clé indiquée est vérifiée en plus, sinon 403. Voir API.
Utilisateurs
| Méthode | Route | Permission | Rôle |
|---|---|---|---|
GET | /lrvo/api/users | users.view | Liste paginée. |
POST | /lrvo/api/users | users.manage | Créer un utilisateur, avec ou sans invitation. |
GET | /lrvo/api/users/:id | users.view | Détail. |
PUT | /lrvo/api/users/:id | users.manage | Modifier name, role, email_verified. |
PUT | /lrvo/api/users/:id/email | users.manage | Changer l'adresse. |
PUT | /lrvo/api/users/:id/password | users.manage | Définir un mot de passe. |
POST | /lrvo/api/users/:id/send-password-reset | users.manage | Envoyer un lien de réinitialisation. |
GET | /lrvo/api/users/:id/roles | users.view | Lister les rôles. |
POST | /lrvo/api/users/:id/roles | users.manage | Assigner un rôle. |
DELETE | /lrvo/api/users/:id/roles/:roleId | users.manage | Révoquer un rôle. |
Profil de l'utilisateur connecté
| Méthode | Route | Rôle |
|---|---|---|
PUT | /lrvo/api/users/profile | Modifier le nom. |
POST | /lrvo/api/users/profile/email | Initier un changement d'adresse. |
GET | /lrvo/api/users/email/confirm | Confirmer le changement. |
POST | /lrvo/api/users/profile/password | Changer le mot de passe. |
POST | /lrvo/api/users/profile/picture | Téléverser une photo. |
DELETE | /lrvo/api/users/profile/picture | Supprimer la photo. |
GET | /lrvo/api/users/profile/sessions | Lister les sessions actives. |
DELETE | /lrvo/api/users/profile/sessions | Révoquer toutes les sessions. |
DELETE | /lrvo/api/users/profile/sessions/:sessionId | Révoquer une session. |
Rôles et permissions
| Méthode | Route | Permission | Rôle |
|---|---|---|---|
GET | /lrvo/api/roles | roles.view | Lister. |
POST | /lrvo/api/roles | roles.manage | Créer. |
GET | /lrvo/api/roles/:id | roles.view | Détail. |
PUT | /lrvo/api/roles/:id | roles.manage | Modifier. |
DELETE | /lrvo/api/roles/:id | roles.manage | Supprimer. |
GET | /lrvo/api/permissions | roles.view | Catalogue des permissions, groupées, sections du core en tête. Voir Utilisateurs. |
Invitation par un admin
Un admin crée un utilisateur depuis /admin/users. Deux flux selon qu'un mot de passe est fourni.
Avec mot de passe : le compte est créé avec email_verified = true et un compte credential. Connexion immédiate possible.
Sans mot de passe : le compte est créé avec email_verified = false et aucun credential. Un token account_setup valide 48 heures est généré et un courriel d'invitation part. L'utilisateur suit le lien, choisit son mot de passe, et le compte est activé en une étape, le passage par le lien vaut confirmation de l'adresse.
Admin crée l'utilisateur (sans mot de passe)
→ POST /lrvo/api/users
→ lrvo_auth_users créé (email_verified: false)
→ token account_setup dans lrvo_auth_verifications (48 h)
→ InvitationEmail envoyé
Utilisateur suit /auth/setup
→ setupAccountAction
→ token vérifié
→ lrvo_auth_accounts créé avec mot de passe haché
→ email_verified = true, token supprimé
→ redirection vers /auth/loginBarrel serveur
@lrvo/core/features/auth réexporte la couche de données de core/users, plus ce qui appartient en propre à la feature. Il n'expose pas les server actions : elles importent next/headers au top-level, donc elles vivent sous .../actions.
| Fonction | Rôle |
|---|---|
isRegistrationOpen() | Indique si les inscriptions publiques sont ouvertes. Voir Fermer les inscriptions. |
needsFirstRun() | true tant qu'aucun compte n'existe. Voir Première configuration. |
createFirstAdmin(userData, sessionOptions) | Crée le premier administrateur et sa session. C'est ce qu'appelle firstRunAction. |
resendEmailVerification(email) | Émet un nouveau token de vérification pour un compte non vérifié. |
EmailNotVerifiedError | Erreur levée par login() sur un compte non vérifié. |
completeAccountSetup({ email, token, password }) | Consomme un token d'invitation, crée le compte credential et marque l'adresse vérifiée. C'est ce qu'appelle setupAccountAction. |
sendVerificationEmail(email, token, baseUrl) | Envoie le courriel de confirmation, vers /auth/confirm. |
sendPasswordResetEmail(email, token, baseUrl) | Envoie le lien de réinitialisation, vers /auth/reset. |
sendPasswordChangedEmail(email) | Notifie un changement de mot de passe. |
sendInvitationEmail(email, token, baseUrl) | Envoie l'invitation, vers /auth/setup. |
resetPassword est réexportée ici avec onPasswordChanged déjà branché sur sendPasswordChangedEmail : celle de @lrvo/core/users change le mot de passe sans prévenir personne. register, elle, se contente de retourner verificationToken ; c'est registerAction qui appelle ensuite sendVerificationEmail. Un envoi en échec est journalisé puis levé par sendVerificationEmail, à l'appelant de décider ce qu'il en fait.
Gabarits de courriel
| Gabarit | Envoi |
|---|---|
VerificationEmail | Après inscription, et à chaque renvoi du lien. |
PasswordResetEmail | Demande de réinitialisation. |
PasswordChangedEmail | Confirmation de changement de mot de passe. |
EmailChangeConfirmEmail | Lien de confirmation vers la nouvelle adresse. |
EmailChangeNotifyEmail | Notification vers l'ancienne adresse. |
InvitationEmail | Création de compte par un admin sans mot de passe. |
Sécurité
Rate limit par IP sur register (partagé avec la première configuration), login, forgot_password, reset_password, setup_account, verify_email et resend_verification. Ces parcours précèdent la session : ils ne bénéficient pas du budget triplé accordé à un appelant connecté sur l'API, sans quoi détenir un cookie élargirait la protection contre la force brute. Quand l'IP est inconnue, parce qu'aucun proxy n'est configuré, le rate limit est suspendu et un avertissement opérateur est émis une seule fois. Derrière un reverse proxy vérifié, renseigner LRVO_TRUST_PROXY.
Champs anti-bot. Chaque formulaire embarque un honeypot (_hp) et un horodatage de chargement (_t). Une soumission de moins de 1,5 seconde, de plus de 10 minutes, ou avec le honeypot rempli est rejetée.
Cookie HttpOnly. Le token de session n'est jamais exposé à JavaScript.
Erreurs opaques. Les erreurs internes sont journalisées côté serveur et remplacées par un message générique. Seules les UserFacingError (token expiré, par exemple) remontent verbatim.
Tables
| Table | Contenu |
|---|---|
lrvo_auth_users | Utilisateurs. |
lrvo_auth_sessions | Sessions actives, avec IP et user-agent. |
lrvo_auth_accounts | Comptes liés à un provider. |
lrvo_auth_verifications | Tokens de vérification et de réinitialisation. |
db.js expose createTables() et dropTables(), appelés par lrvo-db, jamais par initializeLrvo() : le boot du serveur ne touche pas au schéma. La feature auth passe en premier à la création et en dernier à la suppression, parce que les tables des modules référencent lrvo_auth_users en clé étrangère.