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';
RoutePage
/auth/loginConnexion
/auth/registerInscription
/auth/forgotMot de passe oublié
/auth/resetRéinitialisation du mot de passe
/auth/confirmVérification de l'adresse
/auth/setupConfiguration du compte après invitation admin
/auth/logoutDé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.

SegmentTitre
login, ou un segment inconnuConnexion
registerCréer un compte
forgotMot de passe oublié
resetNouveau mot de passe
confirmVérification du courriel
setupActiver le compte
logoutDé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, donc admin.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 :

CheminFonction
Lien de /auth/confirmverifyEmailAction
Réinitialisation du mot de passe par lienresetPassword()
Invitation acceptée sur /auth/setupcompleteAccountSetup()
Création par l'admin avec mot de passePOST /lrvo/api/users
Action manuelle d'un adminPUT /lrvo/api/users/:id avec { email_verified: true }, bouton de la fiche utilisateur
Première configurationcreateFirstAdmin()

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 :

  • registerAction refuse la création et retourne { success: false, error: 'Les inscriptions sont actuellement fermées.' }. Le refus est aussi appliqué dans register() de core/users, donc un module qui appelle la couche de données directement se heurte au même verrou.
  • /auth/register affiche 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_users est vide, le formulaire de première configuration reste disponible, et register() elle-même laisse passer ce compte et lui donne le rôle admin. Sans cette exception, une installation neuve configurée à false n'aurait aucun moyen de créer son administrateur.
  • La création de comptes par un admin. POST /lrvo/api/users et le parcours d'invitation vers /auth/setup ne passent pas par register(). 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.

ChampRôle
emailAdresse.
passwordMot de passe.
nameNom 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)

ChampRôle
emailAdresse.
tokenToken reçu par courriel.
newPasswordNouveau 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.

ChampRôle
emailAdresse.
tokenToken du courriel d'invitation.
newPasswordMot de passe choisi.
confirmPasswordConfirmation.

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éthodeRoutePermissionRôle
GET/lrvo/api/usersusers.viewListe paginée.
POST/lrvo/api/usersusers.manageCréer un utilisateur, avec ou sans invitation.
GET/lrvo/api/users/:idusers.viewDétail.
PUT/lrvo/api/users/:idusers.manageModifier name, role, email_verified.
PUT/lrvo/api/users/:id/emailusers.manageChanger l'adresse.
PUT/lrvo/api/users/:id/passwordusers.manageDéfinir un mot de passe.
POST/lrvo/api/users/:id/send-password-resetusers.manageEnvoyer un lien de réinitialisation.
GET/lrvo/api/users/:id/rolesusers.viewLister les rôles.
POST/lrvo/api/users/:id/rolesusers.manageAssigner un rôle.
DELETE/lrvo/api/users/:id/roles/:roleIdusers.manageRévoquer un rôle.

Profil de l'utilisateur connecté

MéthodeRouteRôle
PUT/lrvo/api/users/profileModifier le nom.
POST/lrvo/api/users/profile/emailInitier un changement d'adresse.
GET/lrvo/api/users/email/confirmConfirmer le changement.
POST/lrvo/api/users/profile/passwordChanger le mot de passe.
POST/lrvo/api/users/profile/pictureTéléverser une photo.
DELETE/lrvo/api/users/profile/pictureSupprimer la photo.
GET/lrvo/api/users/profile/sessionsLister les sessions actives.
DELETE/lrvo/api/users/profile/sessionsRévoquer toutes les sessions.
DELETE/lrvo/api/users/profile/sessions/:sessionIdRévoquer une session.

Rôles et permissions

MéthodeRoutePermissionRôle
GET/lrvo/api/rolesroles.viewLister.
POST/lrvo/api/rolesroles.manageCréer.
GET/lrvo/api/roles/:idroles.viewDétail.
PUT/lrvo/api/roles/:idroles.manageModifier.
DELETE/lrvo/api/roles/:idroles.manageSupprimer.
GET/lrvo/api/permissionsroles.viewCatalogue 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/login

Barrel 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.

FonctionRô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é.
EmailNotVerifiedErrorErreur 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

GabaritEnvoi
VerificationEmailAprès inscription, et à chaque renvoi du lien.
PasswordResetEmailDemande de réinitialisation.
PasswordChangedEmailConfirmation de changement de mot de passe.
EmailChangeConfirmEmailLien de confirmation vers la nouvelle adresse.
EmailChangeNotifyEmailNotification vers l'ancienne adresse.
InvitationEmailCré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

TableContenu
lrvo_auth_usersUtilisateurs.
lrvo_auth_sessionsSessions actives, avec IP et user-agent.
lrvo_auth_accountsComptes liés à un provider.
lrvo_auth_verificationsTokens 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.

Sur cette page