Ajout de l'authentification avec Better Auth dans React Router
Implémente l’auth complète avec Better Auth, Zod et Prisma : sessions sécurisées, rôles admin protégés et hooks React.
Pourquoi intégrer Better Auth ?
Notre back-office admin est prêt, mais aucune authentification n’était encore en place. Dans cette leçon, nous allons utiliser Better Auth pour gérer :
- email + mot de passe,
- sessions cookie signées,
- rôles
customer,administrator,super_administrator.
Objectif : mettre en place une authentification complète, avec SSR, entièrement typée, et réutilisable sur toutes les routes protégées.
Mise à jour des dépendances
1. Installer Better Auth
1npm install better-auth
2. Exigence Zod ≥ 3 .25
1npm install zod@3.25.76
Pourquoi ? Better Auth vérifie les schémas côté serveur avec Zod.
Sans la bonne version la commande npm install better-auth échoue (erreur vue dans la vidéo).
Variables d’environnement
1BETTER_AUTH_URL=http://localhost:5173 # même port que le dev-server2BETTER_AUTH_SECRET=super-secret-key-32-chars
• BETTER_AUTH_SECRET vient de la doc Better Auth (copie / colle le token généré).
• Garder ces clés hors du repo (.gitignore).
Adapter Prisma
Better Auth ajoute les tables : sessions, accounts, verification.
1npx prisma migrate dev --name add-authentication-with-betterauth
Le diff dans schema.prisma :
1+model Session {2+ id String @id3+ token String @unique4+ userId String5+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)6+ expiresAt DateTime7+ createdAt DateTime8+ updatedAt DateTime9+}
Configurer le serveur Better Auth
1import { betterAuth } from "better-auth";2import { prismaAdapter } from "better-auth/adapters/prisma";3import { admin } from "better-auth/plugins";4import { PrismaClient } from "~/../generated/prisma/client";56const prisma = new PrismaClient();78export const auth = betterAuth({9baseURL: process.env.BETTER_AUTH_URL,10secret: process.env.BETTER_AUTH_SECRET,11emailAndPassword: { enabled: true },12database: prismaAdapter(prisma, { provider: "postgresql" }),13plugins: [admin({ adminRoles: ["administrator", "super_administrator"] })],14});
Explications :
prismaAdapterbranche Better Auth sur la DB existante.admin()déclare les rôles qui auront accès au back-office.emailAndPassword.enabled = trueactive la connexion classique.
Route API pour Better Auth
1import { auth } from "~/server/auth.server";23export async function loader({ request }: LoaderFunctionArgs) {4return auth.handler(request);5}6export async function action({ request }: ActionFunctionArgs) {7return auth.handler(request);8}
Cette « resource route » reçoit toutes les requêtes du SDK : signIn, signUp, signOut…
Client React Better Auth
1import { adminClient } from "better-auth/client/plugins";2import { createAuthClient } from "better-auth/react";34export const authClient = createAuthClient({5baseURL: "http://localhost:5173", // même que .env6plugins: [adminClient()],7});89export const { signIn, signUp, signOut, useSession } = authClient;
Le client gère :
- requête vers
/api/auth; - stockage du cookie de session
better-auth.session_token.
Pages Login & Register
Validation Zod × Conform
1import { z } from "zod";23export const RegisterSchema = z4.object({5name: z.string().min(2),6email: z.string().email(),7password: z.string().min(8),8confirmPassword: z.string(),9})10.refine((d) => d.password === d.confirmPassword, {11path: ["confirmPassword"],12message: "Les mots de passe ne correspondent pas",13});1415export const LoginSchema = z.object({16email: z.string().email(),17password: z.string().min(1),18});
Formulaire Register (extrait)
1import { getFormProps, getInputProps, useForm } from "@conform-to/react";2import { parseWithZod } from "@conform-to/zod";3import { signUp } from "~/lib/auth-client";4import { RegisterSchema } from "~/utils/auth-schemas";56const [form, fields] = useForm({7onValidate({ formData }) {8return parseWithZod(formData, { schema: RegisterSchema });9},10});1112async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {13e.preventDefault();14const formData = new FormData(e.currentTarget);15const submission = parseWithZod(formData, { schema: RegisterSchema });16if (submission.status !== "success") return;1718const { error } = await signUp.email(submission.value);19if (!error) navigate("/");20}
Aucune action serveur : le SDK Better Auth gère la session et le cookie.
Helpers d’accès serveur
Obtenir l’utilisateur (optionnel)
1export async function getOptionalUser(request: Request) {2try {3const session = await auth.api.getSession({ headers: request.headers });4return session?.user ?? null;5} catch {6return null;7}8}
Exiger un utilisateur connecté
1import { redirect } from "react-router";23export async function requireUser(request: Request) {4const user = await getOptionalUser(request);5if (!user) {6const url = new URL(request.url);7throw redirect(`/login?redirectTo=${url.pathname}`);8}9return user; // utilisateur typé10}
Exiger un admin
1export async function requireAdmin(request: Request) {2const user = await requireUser(request);3if (!["administrator", "super_administrator"].includes(user.role)) {4await auth.api.signOut({ headers: request.headers }); // purge cookie5throw redirect("/login");6}7return user;8}
Hook global : useOptionalUser()
1export async function loader({ request }: Route.LoaderArgs) {2const user = await getOptionalUser(request);3return data({ user });4}56export function useOptionalUser() {7return useRouteLoaderData<typeof loader>("root")?.user ?? null;8}
– Appelez-le dans la Navbar pour afficher « Bonjour Alice » ou un bouton « Login ».
Protection des routes admin
1export async function loader({ request }: Route.LoaderArgs) {2await requireAdmin(request);3return data(null);4}
Un invité ➜ redirect /login?redirectTo=/admin.
Déconnexion sécurisée
1<button2onClick={() => signOut().then(() => navigate("/"))}3className="text-sm text-gray-600 hover:text-black"4>5Déconnexion6</button>
Le SDK supprime le cookie, le loader root ne renvoie plus d’utilisateur.
Résultat
- Auth complet (inscription, connexion, déconnexion).
- Sessions HTTP-only signées ; cookie nettoyé en cas d’erreur.
- Rôles admin protégés côté serveur (
requireAdmin). - SSR : les loaders savent si l’utilisateur est connecté avant le rendu.
- Pages Login / Register validées client et serveur (Zod + Conform).
Ton back-office admin est désormais inaccessible aux visiteurs et prêt pour la suite du projet.
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?