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.
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
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 :
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.
1npm install better-auth
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).
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
).
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+}
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 :
prismaAdapter
branche Better Auth sur la DB existante.admin()
déclare les rôles qui auront accès au back-office.emailAndPassword.enabled = true
active la connexion classique.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…
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 :
/api/auth
;better-auth.session_token
.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});
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.
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}
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}
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}
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 ».
1export async function loader({ request }: Route.LoaderArgs) {2await requireAdmin(request);3return data(null);4}
Un invité ➜ redirect /login?redirectTo=/admin
.
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.
requireAdmin
).Ton back-office admin est désormais inaccessible aux visiteurs et prêt pour la suite du projet.
Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?