L'authentification avec un starter kit

Sommaire
Dans les TP précédents, vous avez codé à la main toute la mécanique d'authentification : mots de passe hashés, session et middleware, puis le reset de mot de passe (token, expiration, email). Et vous avez vu que la gestion des utilisateurs est un élément complexe d'une application :
- L'authentification.
- Limiter l'accès à certaines ressources.
- La sécurité des mots de passe.
- La gestion du mot de passe oublié.
- La double authentification.
- …
Vous l'avez compris, la liste est longue… La force de Laravel, c'est justement de vous fournir l'ensemble de ces briques, dans les standards du marché, sans tout recoder. Dans ce TP, nous allons générer une application complète avec un starter kit de Laravel, comprendre ce qui tourne derrière (Fortify), puis la modifier pour l'adapter à nos besoins : un profil enrichi, des rôles, des composants récupérés sur HyperUI, et pour finir un petit chat en Ajax.
Dans ce TP, je vous invite à avoir en parallèle :
Les slides
Avant de commencer, voici une présentation rapide de la partie théorie de notre TP du jour : le scaffolding, les starter kits, Fortify, et les outils qui arrivent avec (Tailwind, Vite, composants).
Prérequis
Ce TP repart d'un nouveau projet Laravel, mais il s'appuie sur ce que vous avez construit dans la chaîne des TP Laravel :
- Créer un projet, des routes, des contrôleurs et des vues (TP introduction).
- Les migrations, les modèles et les relations (TP base de données).
- Le fonctionnement d'une authentification : hash, session, middleware (TP Comprendre l'authentification).
Il vous faut également Node.js (et donc npm) sur votre poste : le starter kit en a besoin pour compiler l'interface. Si vous ne l'avez pas, vous pouvez le télécharger ici.
Vous n'avez pas fait le TP Comprendre l'authentification ?
Vous pouvez suivre ce TP sans lui, le starter kit fait tout le travail. Mais vous perdrez le plus intéressant : comprendre ce qui a été généré. Je vous conseille de le faire avant, ou au moins de le lire en parallèle. Même chose pour le TP bonus La double authentification (2FA), que nous comparerons à la 2FA du kit en fin de TP.
Objectifs
À la fin de ce TP vous saurez :
- Créer un projet avec un starter kit (
laravel new) et expliquer le rôle de Fortify. - Retrouver dans le code généré les briques codées à la main dans les TP précédents.
- Utiliser les outils du front qui arrivent avec (Tailwind, Vite,
npm). - Utiliser et créer des composants Blade (
<x-layouts::app>,<x-card>), et en récupérer sur HyperUI. - Protéger une page avec le middleware
auth. - Modifier le code généré : champs supplémentaires, table liée, rôles (relation many-to-many).
- Expliquer le fonctionnement d'une double authentification TOTP (QR code, codes de récupération).
- Rafraîchir une page en Ajax avec
fetchou HTMX.
Breeze, starter kits et Fortify
Si vous avez suivi des tutoriels Laravel, vous avez sûrement croisé Breeze : c'était, jusqu'à Laravel 11, le kit officiel pour générer une authentification. Depuis Laravel 12, il a été remplacé par les starter kits, proposés directement par l'installeur laravel new. Il en existe quatre :
| Kit | Les vues |
|---|---|
| React | React + TypeScript (via Inertia) |
| Vue | Vue + TypeScript (via Inertia) |
| Svelte | Svelte + TypeScript (via Inertia) |
| Livewire | Blade, avec les composants Flux |
Nous allons utiliser le kit Livewire : ses vues sont en Blade, ce que vous connaissez déjà.
Et la partie serveur ? Tous les kits s'appuient sur le même moteur : Laravel Fortify. Fortify fournit les routes, les contrôleurs et toute la sécurité (limitation des tentatives, reset du mot de passe, double authentification…), mais aucune vue : on parle de paquet « headless » (sans tête : sans interface). Le kit, lui, fournit les vues et les branche sur Fortify.
Créer votre projet
L'installeur Laravel
Jusqu'ici, vous avez créé vos projets avec composer create-project. Pour profiter des starter kits, il nous faut l'installeur Laravel, une petite commande laravel à installer une seule fois sur votre poste :
composer global require laravel/installerVérifiez ensuite qu'elle fonctionne :
laravel --versionlaravel : commande introuvable ?
Composer installe les commandes globales dans un dossier qui doit être dans votre PATH :
- Sous Windows :
%USERPROFILE%\AppData\Roaming\Composer\vendor\bin. - Sous Linux :
~/.config/composer/vendor/bin(ou~/.composer/vendor/bin). - Sous macOS :
~/.composer/vendor/bin.
La commande composer global config bin-dir --absolute vous donne le chemin exact sur votre poste. Ajoutez-le à votre PATH, puis rouvrez votre terminal.
Le projet
Lancez maintenant la création du projet :
laravel new mon-projet --phpunitL'option --phpunit choisit PHPUnit comme outil de tests, celui que nous utiliserons dans le TP sur les tests (sans elle, l'installeur choisit Pest). La commande va vous poser quelques questions, voici les réponses à donner :
- Do you want to use a starter kit? :
Yes(flèche de gauche, puis Entrée). - Which frontend stack? :
Livewire(flèche du bas, puis Entrée). - Which authentication provider? :
Laravel's built-in authentication(le choix par défaut). - Single-file Livewire components? :
Yes(le choix par défaut). - Teams support? :
No(le choix par défaut). - Which authentication features? : décochez
Email verificationetPasskeysavec la touche Espace, gardez le reste, puis Entrée.

La vérification de l'email et les passkeys (connexion sans mot de passe, avec l'empreinte du téléphone) sont deux fonctionnalités intéressantes, mais elles ajouteraient des étapes à chaque inscription : nous nous en passons pour ce TP.
L'installation se termine toute seule : création de la base SQLite, migrations, installation des dépendances JavaScript (npm install) et première compilation de l'interface (npm run build). Elle installe aussi Laravel Boost, un outil qui aide les assistants IA (Copilot, Claude…) à comprendre votre projet : vous pouvez l'ignorer.
Pour lancer votre application :
cd mon-projet
composer run devcomposer run dev lance en une seule commande le serveur PHP (php artisan serve), Vite (npm run dev, nous y revenons), et l'affichage des logs. Votre site est disponible sur http://127.0.0.1:8000. Pour tout arrêter : Ctrl+C.
Point de contrôle
http://127.0.0.1:8000 affiche la page d'accueil du kit, avec deux liens Log in et Register en haut à droite. php artisan --version indique Laravel 13.
Installer la Laravel Debugbar
Vous avez remarqué que les pages d'erreurs de Laravel sont toujours pleines de bonnes informations. Avec Laravel nous pouvons avoir « encore mieux » : une extension qui ajoute une barre en bas de votre site (uniquement le temps du développement) et qui vous permet de voir :
- Les requêtes SQL réalisées par l'ORM.
- Les vues affichées.
- Le contenu de la session.
- …
Pour l'installer, dans le dossier de votre projet :
composer require fruitcake/laravel-debugbar --devLa Debugbar est automatiquement active quand APP_DEBUG vaut true dans votre fichier .env. Elle apparaît en bas de chaque page, cliquez sur un onglet pour le déplier. Elle vous sera très utile pour comprendre ce que fait Eloquent derrière vos ->get() et vos relations. Gardez-la sous le coude !
Ce que le kit a généré
Tester le code
Votre site est fonctionnel ! Avant d'aller plus loin, je vous laisse tester l'ensemble :
- La création d'un compte (Register).
- La déconnexion (menu en bas à gauche, Log out), puis la connexion.
- Les réglages (Settings, dans le même menu) : profil, mot de passe, apparence (essayez le mode sombre).

Point de contrôle
Après l'inscription, vous arrivez sur la page /dashboard, avec un menu latéral à gauche. Dans la Debugbar, l'onglet « Session » contient votre identifiant de connexion.
Le tour des fichiers
Prenez quelques minutes pour regarder ce que contient votre projet. Voici les dossiers et fichiers importants, classés par rôle :
Côté Fortify :
config/fortify.php: la configuration, et en bas du fichier la liste des fonctionnalités activées (features). Vous y retrouvez vos choix de l'installation.app/Providers/FortifyServiceProvider.php: c'est ici que les vues du kit sont branchées sur Fortify (méthodeconfigureViews), et que sont réglées les limitations de tentatives (configureRateLimiting).app/Actions/Fortify/: les actions, le code métier exécuté par Fortify :CreateNewUser(inscription) etResetUserPassword.app/Concerns/: les règles de validation partagées (mot de passe, profil).
Côté interface :
routes/web.phpetroutes/settings.php: vos routes (accueil, dashboard, réglages).resources/views/pages/auth/: les vues de connexion, d'inscription, de mot de passe oublié…resources/views/pages/settings/: les pages de réglages, sous forme de composants Livewire (les fichiers⚡).resources/views/layouts/etresources/views/components/: les layouts et les composants de l'interface (nous y revenons plus loin).
Ce qu'apporte Livewire
Si vous avez déjà vu un projet Breeze, trois éléments vont vous surprendre. Tous viennent de Livewire, la bibliothèque qui permet d'écrire des pages dynamiques en PHP et en Blade, sans écrire de JavaScript.
Les espaces de noms pages:: et layouts::. Dans le provider de Fortify, vous lisez view('pages::auth.login'), et dans les vues <x-layouts::app>. Ce sont des raccourcis vers des dossiers, déclarés par Livewire :
| Vous écrivez | Le fichier utilisé |
|---|---|
view('pages::auth.login') | resources/views/pages/auth/login.blade.php |
<x-layouts::app> | resources/views/layouts/app.blade.php |
<x-layouts::auth> | resources/views/layouts/auth.blade.php |
Le :: sépare l'espace de noms du chemin, et le . sépare les sous-dossiers, comme pour vos composants <x-…>.
Les fichiers ⚡. Ce sont des composants Livewire « mono-fichier » : en haut, une classe PHP (new class extends Component { … }) avec ses propriétés et ses méthodes ; en bas, le HTML en Blade. Les attributs wire: relient les deux : wire:model="name" lie un champ à la propriété $name, wire:submit="updateProfileInformation" appelle la méthode PHP du même nom à l'envoi du formulaire. Livewire fait l'aller-retour avec le serveur en Ajax et met la page à jour, sans la recharger. Le ⚡ ne fait pas partie du nom : il sert uniquement à repérer les composants d'un coup d'œil (⚡profile.blade.php s'appelle pages::settings.profile).
Route::livewire(). Dans routes/settings.php, Route::livewire('settings/profile', 'pages::settings.profile') affiche un composant Livewire comme une page entière, placée automatiquement dans le layout layouts::app.
Nous ne modifierons pas les composants Livewire dans ce TP : nos pages resteront des contrôleurs et des vues Blade classiques, que vous savez déjà écrire. Mais vous savez maintenant les lire.
Question :
- Ouvrez
resources/views/pages/settings/⚡profile.blade.php. Que se passe-t-il quand on clique sur Save ? Retrouvez la méthode appelée et ce qu'elle fait.
Voir la réponse
Le formulaire porte wire:submit="updateProfileInformation" : Livewire appelle la méthode updateProfileInformation() de la classe PHP, en haut du fichier. Elle valide les propriétés $name et $email (remplies par les wire:model), met à jour l'utilisateur ($user->fill(...) puis $user->save()), et affiche une notification avec Flux::toast(...). Pas de route, pas de contrôleur, pas de redirection : tout passe par Livewire.
Les routes de Fortify
Et les routes de connexion ? Elles ne sont pas dans routes/web.php : elles sont déclarées par Fortify, dans le paquet. Pour toutes les voir :
php artisan route:listVous y retrouvez login, logout, register, forgot-password, reset-password, two-factor-challenge…
Que se passe-t-il derrière ?
Le POST /login de votre formulaire arrive dans un contrôleur de Fortify, qui fait exactement ce que vous aviez codé à la main dans le TP Comprendre l'authentification : il valide les champs, vérifie le hash avec Auth::attempt(), régénère la session ($request->session()->regenerate(), contre la fixation de session), puis vous redirige vers le dashboard. Avec, en prime, la limitation des tentatives.
Questions :
- Ouvrez
app/Providers/FortifyServiceProvider.php. Quelle vue est affichée quand on demande la page/login? - Ouvrez
app/Actions/Fortify/CreateNewUser.php. Le mot de passe est passé tel quel auUser::create(), sansHash::make(). Est-il pour autant stocké en clair ? (Indice : regardez la méthodecasts()du modèleUser.) - Toujours dans le provider, que fait le
RateLimiter::for('login', …)?
Voir les réponses
Fortify::loginView(fn () => view('pages::auth.login')): la vueresources/views/pages/auth/login.blade.php. Fortify ne sait pas afficher de formulaire, il appelle cette fonction pour obtenir la vue. LePOST /loginest ensuite traité par Fortify.- Non : le modèle
Userdéclare'password' => 'hashed'dans sescasts(). Laravel hashe automatiquement le mot de passe au moment de l'enregistrement. Vérifiez dans votre base : la colonnepasswordcommence par$2y$, c'est bien un hash bcrypt, comme dans votre TP. - Il limite la connexion à 5 tentatives par minute pour un même couple email + adresse IP : c'est la protection contre la force brute, que vous avez vue dans le TP sécurité sur l'authentification.
L'UI : Tailwind et Vite
Les vues du kit reposent sur un « framework UI » autre que Bootstrap : Tailwind. Tailwind poursuit le même objectif que Bootstrap, mais avec une approche différente : des petites classes CSS que l'on va appeler « utilitaires » (text-center, font-bold, p-6…), que vous composez directement dans votre HTML.
C'est pour ça que nous avons besoin de node et surtout de npm : Vite va lire vos templates, ne garder que les classes Tailwind réellement utilisées, puis « compiler » / « packager » l'ensemble dans un fichier CSS et un fichier JS optimisés pour vos clients. Rien d'étonnant à croiser ces outils du monde JavaScript dans un projet PHP : c'est partout aujourd'hui.
Les commandes à connaître, dans le dossier de votre projet :
# Installer les dépendances (déjà fait par l'installeur, à refaire après un git clone)
npm install
# Compiler une fois les ressources (pour la mise en production)
npm run build
# Ou recompiler automatiquement à chaque modification (pendant le développement)
npm run devnpm run build ?
La commande npm run build fige vos ressources à un instant donné. Si vous ajoutez une classe Tailwind dans un template après le build, elle n'aura aucun effet tant que vous ne relancez pas la compilation.
Pendant le développement, utilisez composer run dev : il lance Vite en mode dev à côté du serveur PHP, et vos classes sont prises en compte à chaque enregistrement.
Où se configure Tailwind ? Dans resources/css/app.css. La ligne @source '../views'; indique à Tailwind d'aller chercher les classes utilisées dans tous vos templates : vos futures vues et vos futurs composants sont donc pris en compte automatiquement.
Les composants et les templates Blade
Si vous avez regardé le code des vues, vous avez remarqué une syntaxe un peu particulière : des balises qui commencent par x- (<x-layouts::app>, <x-auth-header>…) et d'autres par flux: (<flux:input>, <flux:button>…). Ce sont des composants Blade. Ils permettent de créer des morceaux de vue réutilisables, mais également des layouts de base.
- Les balises
x-…sont des composants de votre projet : des fichiers dansresources/views/components/(ou dansresources/views/layouts/pour la syntaxex-layouts::…). - Les balises
flux:…sont les composants de Flux, la bibliothèque d'interface utilisée par le kit : champs, boutons, menus, modales… La version gratuite suffit largement pour ce TP.
Pour plus d'informations sur les composants, rendez-vous ici
Nous avons déjà vu l'héritage
Dans le TP introduction, nous avions vu l'héritage de template (@extends, @section, @yield). Ici c'est la même idée, mais via un système de « composants », très à la mode en ce moment. Les deux fonctionnent, personnellement je trouve l'approche composant plus moderne et plus lisible.
Avec le kit, nous avons à notre disposition deux layouts :
layouts/app.blade.phppour les clients connectés, utilisable dans votre template via<x-layouts::app>. Il contient le menu latéral (danslayouts/app/sidebar.blade.php).layouts/auth.blade.phppour les pages de connexion et d'inscription, utilisable via<x-layouts::auth>.
Les deux acceptent un attribut title, qui devient le titre de l'onglet : <x-layouts::app title="Mon titre">.
Hey toi lecteur ! Stop !
L'organisation est tout aussi importante que le code. Vous pouvez copier-coller du code entre tous les templates, oui… Mais ce n'est pas efficace, et si on vous demande une modification vous perdrez du temps !
Prendre du temps pour utiliser le système de layout proposé par Laravel est un vrai gain de temps. Le vous du futur vous remercie d'avance.
Fonctionnement des composants
Vous avez utilisé les composants, mais avez-vous compris le fonctionnement ? Un composant, c'est quelque chose de très simple : un morceau de vue réutilisable (un bouton, une card, etc.), donc du « code HTML ». Si vous voulez une comparaison, c'est une sorte de fonction. Prenons un exemple.
Nous souhaitons faire une Card. Créez le fichier resources/views/components/card.blade.php :
<div class="rounded-xl border border-neutral-200 bg-white p-6 shadow-sm dark:border-neutral-700 dark:bg-neutral-900">
{{ $slot }}
</div>L'important ici est {{ $slot }}. $slot est une variable qui va contenir les éléments placés à l'intérieur de votre Card lors de son utilisation :
<x-card>
Ceci est une démo
</x-card>Dans ce cas, la variable $slot contiendra… Ceci est une démo. Vous venez en quelque sorte de créer votre propre élément HTML réutilisable, comme une div, un span ou un li. Remarquez aussi les classes dark:… : elles ne s'appliquent qu'en mode sombre, votre card suit donc le réglage d'apparence du kit.
Question :
- Le nom du fichier est
card.blade.phpet la balise est<x-card>. Comment s'appellerait la balise d'un composantresources/views/components/forms/bouton-rouge.blade.php?
Voir la réponse
<x-forms.bouton-rouge> : le préfixe x-, puis le chemin depuis components/, les sous-dossiers étant séparés par un point.
Je vous laisse parcourir les composants déjà disponibles dans le dossier components (regardez par exemple auth-header.blade.php, et la façon dont il reçoit son titre et sa description avec @props).
Pour résumer
Le système de composants va vous permettre :
- De rendre générique un morceau de code que vous utilisez plusieurs fois dans votre projet.
- De penser composant plutôt que HTML copié depuis Internet.
- De rendre « génériques » les layouts pour les partager entre les pages.
Des composants tout prêts : HyperUI
Écrire ses composants, c'est bien. Mais pour une jolie carte, un tableau ou une alerte, inutile de partir d'une page blanche : des sites proposent des composants Tailwind prêts à copier. Parmi eux, HyperUI : une collection gratuite et open source (licence MIT) de composants Tailwind, rangés en trois familles :
- Application : pour les interfaces d'application (statistiques, tableaux, formulaires, menus…).
- Marketing : pour les sites vitrines (bannières, cartes, témoignages…).
- Neobrutalism : un style graphique plus marqué.
Chaque composant est livré en HTML, avec un bouton pour copier le code. Pas de paquet à installer : comme votre projet utilise déjà Tailwind, il suffit de coller le HTML.
Nous allons ajouter des statistiques sur le dashboard. Rendez-vous dans Application > Stats, choisissez un modèle (pour ma part, le troisième, avec un titre, une valeur et un badge d'évolution) et copiez le code d'une seule carte, c'est-à-dire le premier bloc <article>…</article>.
Créez le fichier resources/views/components/stat.blade.php et collez-y le code copié sur HyperUI. C'est tout : le fichier contient uniquement le HTML du composant, sans <html> ni <body>.
Puis utilisez-le dans resources/views/dashboard.blade.php, par exemple tout en haut de la page, à l'intérieur de <x-layouts::app> :
<x-stat />
Ça fonctionne… mais votre composant affiche toujours « Profit » et « $240.94 ». Pour le rendre réutilisable, transformons ces textes en paramètres. Dans stat.blade.php :
- Ajoutez tout en haut du fichier la ligne
@props(['titre', 'valeur']). - Remplacez le texte « Profit » par
{{ $titre }}. - Remplacez le montant « $240.94 » par
{{ $valeur }}. - Supprimez le badge d'évolution (le
<div>qui contient le pourcentage), dont nous n'avons pas l'usage.
@props(['titre', 'valeur']) déclare les attributs acceptés par le composant : ils deviennent les variables $titre et $valeur. Vous pouvez maintenant l'utiliser autant de fois que nécessaire :
<div class="mb-6 grid gap-4 md:grid-cols-2">
<x-stat titre="Personnes inscrites" valeur="12" />
<x-stat titre="Messages dans le chat" valeur="42" />
</div>Pour l'instant les valeurs sont écrites en dur. Nous leur passerons de vraies valeurs quand le dashboard aura son contrôleur.
Point de contrôle
Le dashboard affiche vos deux cartes de statistiques côte à côte. Si une carte apparaît sans style, vérifiez que composer run dev tourne (ou relancez npm run build).
Et le mode sombre ? Les composants HyperUI sont prévus pour un fond clair (bg-white, text-gray-900). Si vous passez le kit en mode sombre, vos cartes resteront blanches. HyperUI propose une version sombre de la plupart de ses composants : vous pouvez fusionner les deux en ajoutant les classes sombres préfixées par dark:, comme dans notre <x-card>.
Protéger une page
Le but de l'authentification est de limiter l'accès à certaines pages, vous l'avez bien compris… Voyons comment le faire sur nos propres pages. Nous allons créer un contrôleur de test :
php artisan make:controller YoloControllerVous venez de créer un contrôleur vide. En reprenant si nécessaire les exemples du TP introduction, je vous laisse y créer :
| Route | Méthode | Accès |
|---|---|---|
/yolo | publicView() | Tout le monde |
/yolo-connected | privateView() | Utilisateurs connectés |
La partie publique
Pour la partie publique (non connecté), rien de nouveau par rapport aux TP précédents :
- Une méthode
publicView, qui retourne une vueyolo.publicViewaffichant pour l'instantBonjour non connecté. Attention ! Cette vue doit utiliser le layout<x-layouts::auth>. - La route correspondante dans le fichier
routes/web.php.
Besoin d'aide ?
Avez-vous bien cherché ?
Pour la route :
use App\Http\Controllers\YoloController;
Route::get('/yolo', [YoloController::class, 'publicView']);Pour la méthode :
public function publicView()
{
return view('yolo.publicView');
}Et pour la vue resources/views/yolo/publicView.blade.php :
<x-layouts::auth title="Yolo">
<div class="text-center font-bold text-indigo-600">Bonjour Mr Anonyme 👋</div>
</x-layouts::auth>
La partie privée
Pour la partie privée, je vous laisse gérer l'ensemble de la base :
- La création de la méthode
privateViewdans le contrôleur. - La création du template
resources/views/yolo/privateView.blade.php. Attention, cette fois-ci nous allons utiliser le layout<x-layouts::app>, qui contient toute la logique de la partie « information de connexion » (menu, nom de l'utilisateur, déconnexion…).
Pour la vue, j'en profite pour utiliser notre composant <x-card> et un titre Flux :
<x-layouts::app title="Yolo">
<flux:heading size="xl" class="mb-6">Hello</flux:heading>
<x-card>
<div class="text-center font-bold text-indigo-600">
Bonjour {{ Auth::user()->name }} 👋
</div>
</x-card>
</x-layouts::app>{{ Auth::user()->name }} vous donne accès à l'utilisateur connecté, dans une vue comme dans un contrôleur. Vous l'aviez déjà utilisé dans le TP Comprendre l'authentification.
La route
C'est la partie qui nous intéresse… Vous avez remarqué que dans les lignes précédentes, nous n'avons jamais parlé d'authentification. Comment alors la page peut-elle n'être accessible qu'aux gens connectés ?
C'est simple : avec un middleware, comme celui que vous aviez écrit vous-même (CheckAuth). Laravel en fournit un tout prêt, nommé auth. Il intercepte la requête avant le contrôleur et vérifie que l'utilisateur est connecté :
Ajoutez dans le fichier routes/web.php :
Route::get('/yolo-connected', [YoloController::class, 'privateView'])->middleware(['auth'])->name('yoloPrivate');Ici la protection est ajoutée grâce à ->middleware(['auth']). Le ->name('yoloPrivate') donne un nom à la route, pour pouvoir la retrouver avec route('yoloPrivate').
Que se passe-t-il derrière ?
Le middleware auth fait exactement ce que faisait votre CheckAuth : un Auth::check(), et une redirection vers la page de connexion si ce n'est pas le cas. Petit bonus : après la connexion, Laravel vous renvoie automatiquement sur la page que vous vouliez voir au départ.
Regardez aussi la route dashboard du kit : elle est dans un groupe Route::middleware(['auth', 'verified']). Le second middleware vérifie que l'email a été confirmé : comme nous avons désactivé la vérification d'email à l'installation, il ne bloque rien.
Le lien dans le menu
Il reste à ajouter un lien « Yolo » dans le menu latéral. Ouvrez resources/views/layouts/app/sidebar.blade.php et ajoutez un <flux:sidebar.item> juste après celui du Dashboard :
<flux:sidebar.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:sidebar.item>
<flux:sidebar.item icon="face-smile" :href="route('yoloPrivate')" :current="request()->routeIs('yoloPrivate')" wire:navigate>
Yolo
</flux:sidebar.item>Quelques explications :
icon="face-smile": le nom d'une icône Heroicons, la bibliothèque d'icônes utilisée par Flux.:currentmet le lien en valeur quand on est sur la page correspondante.wire:navigate: Livewire charge la page suivante en arrière-plan et ne remplace que son contenu, sans recharger tout le site. La navigation est plus fluide.

Point de contrôle
/yolos'affiche avec ou sans connexion./yolo-connectedredirige vers/loginsi vous n'êtes pas connecté, et affiche votre nom sinon.- Le lien « Yolo » apparaît dans le menu latéral une fois connecté.
👋 Si vous avez des questions, n'hésitez pas. Vous avez maintenant la base de ce qu'il faut savoir. La suite du TP consiste à modifier le code généré : c'est là que vous allez vraiment en comprendre le fonctionnement.
Modifier les informations demandées
En reprenant ce que vous avez vu dans le TP base de données, je vous laisse ajouter dans la table users un nouveau champ nommé pseudo, demandé à l'inscription :
- Ajouter la colonne en base, avec une migration.
- Modifier le formulaire de création de compte
resources/views/pages/auth/register.blade.phppour ajouter le champ. - Modifier le modèle
app/Models/User.phppour autoriser le remplissage du nouveau champ. - Modifier l'action
app/Actions/Fortify/CreateNewUser.phppour valider et sauvegarder la nouvelle valeur.
Pour la migration :
php artisan make:migration add_pseudo_to_users_table --table=usersPuis dans votre migration :
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('pseudo')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('pseudo');
});
}Pourquoi nullable() ?
Votre table users contient déjà des comptes (ceux créés pour tester). Que mettre dans la colonne pseudo de ces comptes ? Sans valeur par défaut, la base refuse d'ajouter une colonne obligatoire : SQLite répond Cannot add a NOT NULL column with default value NULL. Le nullable() règle le problème, et c'est la validation du formulaire qui rendra le pseudo obligatoire pour les nouveaux comptes.
Côté modèle, attention à une nouveauté de Laravel 13 : le modèle User n'a plus de tableau $fillable, mais un attribut PHP placé juste au-dessus de la classe : #[Fillable(['name', 'email', 'password', 'pseudo'])].
Pour le formulaire, réutilisez le composant <flux:input>, comme les autres champs. Bonne surprise : il affiche tout seul le message d'erreur de validation de son champ, pas besoin de @error.
Besoin d'aide pour le formulaire ?
<!-- Pseudo -->
<flux:input
name="pseudo"
label="Pseudo"
:value="old('pseudo')"
type="text"
required
/>Et dans l'action, c'est là que la logique de Fortify devient intéressante. Le contrôleur de l'inscription reste dans le paquet : vous ne le modifiez jamais. Fortify lui demande simplement d'appeler votre action CreateNewUser, qui valide et crée l'utilisateur. C'est donc elle qu'il faut compléter : une règle de validation de plus dans le Validator::make() ('pseudo' => ['required', 'string', 'max:50']), et une ligne de plus dans le User::create().
Question :
- Pourquoi Fortify vous fait-il modifier une action plutôt qu'un contrôleur ?
Voir la réponse
Le contrôleur s'occupe du HTTP (lire la requête, rediriger, répondre en HTML ou en JSON), l'action s'occupe du métier (valider et créer l'utilisateur). En séparant les deux, Fortify peut mettre à jour ses contrôleurs (corrections de sécurité, nouvelles fonctionnalités) avec un simple composer update, sans jamais écraser votre code, qui reste dans app/Actions.
Pour afficher le pseudo de l'utilisateur connecté, c'est comme pour le nom : {{ Auth::user()->pseudo }}.
Créer plusieurs enregistrements en base de données
Le but du kit est de vous permettre de modifier le code « préfourni », c'est ce que l'on appelle du scaffolding. Nous allons en profiter pour modifier son comportement afin de créer, lors de l'inscription, une autre entrée en base de données : une personne, qui contient des informations complémentaires sur l'utilisateur.
- Créez la migration ainsi que le modèle :
php artisan make:model Personne --migration. - Renseignez les champs dans votre migration mais également dans le
$fillabledu modèle. Exemple de migration :
Schema::create('personnes', function (Blueprint $table) {
$table->id();
$table->string('ville');
$table->date('date_naissance');
$table->string('genre');
$table->foreignId('user_id')->constrained();
$table->timestamps();
});- Lancez la migration :
php artisan migrate. - Ajoutez les champs
ville,date_naissance(untype="date") etgenredans le formulaire d'inscription. Pour le genre, Flux propose une liste déroulante :
<flux:select name="genre" label="Genre" required>
<flux:select.option value="">Choisir…</flux:select.option>
<flux:select.option value="femme" :selected="old('genre') === 'femme'">Femme</flux:select.option>
<flux:select.option value="homme" :selected="old('genre') === 'homme'">Homme</flux:select.option>
<flux:select.option value="autre" :selected="old('genre') === 'autre'">Autre</flux:select.option>
</flux:select>- Modifiez l'action
CreateNewUserpour créer la personne, après la création de l'utilisateur (il nous faut sonid). Il faut donc garder l'utilisateur dans une variable au lieu de le retourner directement :
$user = User::create([
'name' => $input['name'],
'pseudo' => $input['pseudo'],
'email' => $input['email'],
'password' => $input['password'],
]);
Personne::create([
'ville' => $input['ville'],
'date_naissance' => $input['date_naissance'],
'genre' => $input['genre'],
'user_id' => $user->id,
]);
return $user;N'oubliez pas le use App\Models\Personne; en haut du fichier. Et voilà : l'inscription crée maintenant un utilisateur et une personne.
Pourquoi user_id ?
foreignId('user_id')->constrained() crée la colonne et la clé étrangère vers la table users. Laravel devine la table grâce au nom de la colonne : user_id → table users. En respectant cette convention, vous n'aurez rien à préciser dans vos relations.
Il reste un point essentiel avant de tester.
N'oubliez pas la validation
Les données que vous récupérez dans le formulaire doivent être validées. Laravel propose un système de validation très complet, je vous laisse regarder la liste des règles disponibles pour compléter le Validator::make() de l'action. Par exemple, pour la date : 'date_naissance' => ['required', 'date', 'before:today'].
Je vous laisse implémenter la modification dans votre code. Pour ma part, voici le formulaire obtenu :

Point de contrôle
Créez un nouveau compte : la table personnes contient une ligne dont le user_id correspond au nouvel utilisateur. Et si vous oubliez un champ, le message d'erreur s'affiche sous le champ concerné.
Avant de passer à la suite, lancez php artisan test.
Le kit a aussi généré des tests
Le test test_new_users_can_register échoue maintenant. Normal, il envoie un formulaire d'inscription sans vos nouveaux champs obligatoires ! Je vous laisse le corriger dans tests/Feature/Auth/RegistrationTest.php, en ajoutant les champs manquants dans le $this->post(route('register.store'), [...]). Nous reviendrons sur les tests dans le TP dédié.
Afficher les informations de l'utilisateur
Maintenant que nous créons une personne lors de l'inscription, nous allons afficher ses informations sur le dashboard après la connexion.
Pour l'instant, la route dashboard du kit affiche directement la vue (Route::view), sans contrôleur. Nous allons donc :
- Créer un contrôleur
DashboardController(php artisan make:controller DashboardController) avec une méthodeindex. - Modifier la route
dashboardpour qu'elle utilise ce contrôleur (en la laissant dans son groupe de middlewares, avec son nom). - Modifier la vue
resources/views/dashboard.blade.phppour afficher les informations de la personne connectée, et donner enfin de vraies valeurs à nos composants<x-stat>.
Le plus propre est de déclarer la relation dans le modèle User. Une personne par utilisateur, c'est une relation un à un (hasOne) :
public function personne()
{
return $this->hasOne(Personne::class);
}Vous pourrez alors récupérer la personne de l'utilisateur connecté avec Auth::user()->personne.
Besoin d'aide pour le contrôleur et la route ?
use App\Models\Personne;
use Illuminate\Support\Facades\Auth;
class DashboardController extends Controller
{
public function index()
{
$personne = Auth::user()->personne;
return view('dashboard', [
'personne' => $personne,
'nbPersonnes' => Personne::count(),
]);
}
}Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
});Et dans la vue, l'attribut précédé de : reçoit une expression PHP au lieu d'un texte : <x-stat titre="Personnes inscrites" :valeur="$nbPersonnes" />.
C'est à vous de jouer pour la vue ! Attention : les comptes créés avant cette étape n'ont pas de personne, $personne vaudra alors null. Pensez à un @if.

Utiliser le système de mapping des paramètres de route
En cours, nous avons vu qu'il était possible de passer des paramètres dans des routes. Pour l'instant, vous vous êtes contentés de récupérer des paramètres simples (id, slug, etc.). Mais il est possible de faire plus pratique.
- Créer une nouvelle route
/personne/{personne}qui va afficher les informations d'une personne. - Créer une méthode
showdans un contrôleurPersonneControllerqui va récupérer les informations de la personne. - Créer une vue
resources/views/personne.blade.phpqui va afficher les informations de la personne.
Je vous laisse créer la structure de cette demande, en protégeant la route avec le middleware auth.
Vous avez la base
Comment gagner du temps ? En laissant Laravel récupérer l'objet en base pour vous ! Il suffit de typer le paramètre de la méthode avec le modèle Personne :
public function show(Personne $personne)
{
return view('personne', ['personne' => $personne]);
}Et voilà : Laravel récupère automatiquement la Personne correspondant à l'id passé dans l'URL (/personne/1). Si elle n'existe pas, il répond directement par une erreur 404. Pas de find(), pas de if, c'est ce que l'on appelle le Route Model Binding.
Question :
- Pour que cela fonctionne, deux noms doivent être identiques. Lesquels ?
Voir la réponse
Le nom du paramètre dans la route ({personne}) et le nom de la variable dans la méthode ($personne). Si vous écrivez {id} dans la route, Laravel ne fera pas le lien.
Dans la vue, vous pouvez afficher le nom de l'utilisateur lié en ajoutant la relation inverse dans le modèle Personne :
public function user()
{
return $this->belongsTo(User::class);
}Liste des personnes
Maintenant que nous avons la possibilité de voir une personne, nous allons créer une page qui affiche la liste de toutes les personnes enregistrées dans la base de données.
Je vous laisse créer une nouvelle route /personnes (méthode index du PersonneController) qui affiche la liste des personnes, avec pour chacune un lien vers sa page /personne/{id}. Ajoutez-la au menu latéral (l'icône users fera l'affaire).

Ouvrez la Debugbar
Affichez votre liste avec Personne::all(), puis regardez l'onglet « Queries » : une requête pour les personnes, puis une requête par personne pour récupérer son utilisateur. Avec 100 personnes, 101 requêtes ! C'est le problème dit « N+1 ».
Remplacez par Personne::with('user')->get() et regardez à nouveau : deux requêtes seulement, quel que soit le nombre de personnes.
Création d'une table pivot pour les rôles
Dans cette partie, nous allons créer une table pivot pour gérer les rôles des utilisateurs. Pour cela, nous allons créer une table roles qui contient les différents rôles disponibles, et une table role_user qui fait le lien entre les utilisateurs et les rôles.
La relation entre les utilisateurs et les rôles est une relation many-to-many (plusieurs à plusieurs) : un utilisateur peut avoir plusieurs rôles, et un rôle peut être attribué à plusieurs utilisateurs. Elle se représente de la façon suivante :
+-----------+ +-----------+ +-----------+
| users | | role_user | | roles |
+-----------+ +-----------+ +-----------+
| id | | user_id | | id |
| name | | role_id | | name |
| … | | | | |
+-----------+ +-----------+ +-----------+
users.id <----> role_user.user_id
roles.id <----> role_user.role_idVous aviez déjà construit une relation de ce type dans le TP base de données : c'est le même principe.
Créer les tables
Pour commencer, créons la table roles avec sa migration (php artisan make:migration create_roles_table), avec un champ name :
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});Ensuite, la table pivot role_user (php artisan make:migration create_role_user_table) qui fait le lien entre les utilisateurs et les rôles :
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignId('role_id')->constrained();
$table->timestamps();
});Le nom role_user n'est pas choisi au hasard
Par convention, Laravel nomme la table pivot avec les deux noms de modèles au singulier, dans l'ordre alphabétique, séparés par un _ : role + user → role_user. En respectant cette convention, Laravel trouve la table tout seul.
Lancez les migrations : php artisan migrate.
Créer le modèle et les relations
Créons le modèle Role (php artisan make:model Role), avec le $fillable et la relation belongsToMany avec le modèle User :
class Role extends Model
{
protected $fillable = ['name'];
public function users()
{
return $this->belongsToMany(User::class);
}
}Et dans le modèle User, la relation belongsToMany avec le modèle Role :
public function roles()
{
return $this->belongsToMany(Role::class);
}Question :
- Nous n'avons pas créé de modèle
RoleUserpour la table pivot. Pourquoi n'en a-t-on pas besoin ?
Voir la réponse
belongsToMany gère la table pivot pour nous : il sait la lire (pour retrouver les rôles d'un utilisateur) et y écrire (avec attach, detach et sync, que nous allons utiliser). Un modèle pour la table pivot n'est utile que si elle porte des données supplémentaires que l'on veut manipuler.
Créer les rôles en base de données
Pour créer les rôles en base de données, nous allons les ajouter au début de la méthode run du seeder database/seeders/DatabaseSeeder.php :
public function run(): void
{
// Les rôles de l'application (firstOrCreate : pas de doublon si on relance le seeder)
Role::firstOrCreate(['name' => 'admin']);
Role::firstOrCreate(['name' => 'user']);
// … le reste du seeder
}N'oubliez pas le use App\Models\Role; en haut du fichier. Exécutez ensuite le seeder : php artisan db:seed.
Seulement la première fois !
Le seeder par défaut crée aussi un utilisateur test@example.com. Si vous relancez db:seed, cette création échoue (l'email existe déjà). Pour repartir d'une base propre : php artisan migrate:fresh --seed (attention, cette commande vide toute la base).
Affecter un rôle à un utilisateur lors de la création
Dans l'action CreateNewUser, ajoutez après la création de l'utilisateur (avant le return $user;) :
// Rôle « user » par défaut
$user->roles()->attach(Role::where('name', 'user')->first());La méthode attach ajoute un rôle à un utilisateur : elle insère une ligne dans la table role_user pour faire le lien. Nous recherchons le rôle par son nom plutôt que d'écrire son id en dur : rien ne garantit que le rôle user porte l'id 2 dans toutes les bases.
Afficher les rôles de l'utilisateur
Pour tester, ajoutez une page /roles (nommée roles) qui affiche les rôles de l'utilisateur connecté. Vous pouvez ajouter une méthode roles dans le contrôleur DashboardController :
public function roles()
{
$roles = Auth::user()->roles;
return view('roles', ['roles' => $roles]);
}Je vous laisse écrire la vue roles.blade.php. Cette page doit être disponible uniquement pour les utilisateurs connectés et visible dans le menu.
Super bouton
Pour tester notre système de rôles, nous allons ajouter un « super bouton » qui permet à l'utilisateur de devenir administrateur. Ajoutez une méthode glowUpAsAdmin dans le contrôleur DashboardController, elle remplace le rôle de l'utilisateur par le rôle d'administrateur :
public function glowUpAsAdmin()
{
$admin = Role::where('name', 'admin')->first();
Auth::user()->roles()->sync([$admin->id]);
return redirect()->route('roles');
}Pourquoi sync ?
sync synchronise les rôles de l'utilisateur avec la liste passée en paramètre : les rôles absents de la liste sont retirés, les nouveaux sont ajoutés. Après l'appel, l'utilisateur n'a que le rôle admin.
Cette action modifie des données : la route doit donc être en POST (protégée par le middleware auth), et le bouton doit être dans un formulaire avec @csrf :
Route::post('/glow-up', [DashboardController::class, 'glowUpAsAdmin'])->middleware(['auth'])->name('glowUp');<form method="POST" action="{{ route('glowUp') }}">
@csrf
<flux:button type="submit" variant="primary">Devenir administrateur 🚀</flux:button>
</form>Question :
- Pourquoi ne pas avoir fait un simple lien
<a href="/glow-up">avec une route enGET?
Voir la réponse
Une route GET ne doit jamais modifier de données. Un simple lien peut être déclenché sans le vouloir (une image <img src="/glow-up"> sur un autre site, un navigateur qui précharge les liens…). En POST, Laravel exige le jeton CSRF : la requête ne peut venir que de votre formulaire.

Évidemment, ce bouton n'a rien à faire dans une vraie application. Il nous servira à tester la partie suivante 😉.
Ajouter un système de chat
Pour la suite de ce TP, je vais vous demander d'ajouter un système de chat à votre application. Ce chat doit être accessible uniquement aux utilisateurs connectés.
Voici les fonctionnalités que vous devez implémenter :
- Un utilisateur connecté peut envoyer un message visible par tous les autres utilisateurs.
- Les messages sont disponibles dans une page
/chat, accessible depuis le menu. Elle doit s'intégrer visuellement à votre application (<x-layouts::app>). - Les messages sont stockés en base de données (une table
messages, liée àusers). - Les messages sont affichés dans l'ordre chronologique.
- Chaque message affiche le nom de l'utilisateur qui l'a envoyé, la date et l'heure d'envoi, et le contenu du message.
- Les utilisateurs avec le rôle d'administrateur peuvent supprimer les messages.
- Le nombre de messages s'affiche dans la seconde carte
<x-stat>du dashboard.
Rôle d'administrateur ?
Pour vérifier si un utilisateur a le rôle d'administrateur, vous pouvez utiliser la relation roles du modèle User :
if (Auth::user()->roles->contains('name', 'admin')) {
// L'utilisateur a le rôle d'administrateur
}Vous allez en avoir besoin à deux endroits (la vue pour afficher le bouton, le contrôleur pour refuser la suppression avec un abort(403)) : pourquoi ne pas en faire une méthode isAdmin() dans le modèle User ?
Je vous laisse construire votre chat. Si vous ne savez pas par quel bout le prendre, voici une proposition d'ordre.
Coup de pouce : par où commencer ?
- Le modèle et la migration :
php artisan make:model Message --migration, avec unuser_id(comme pourPersonne) et un champcontenu. - Un
ChatControlleravec trois méthodes :index(afficher la page),store(enregistrer un message) etdestroy(supprimer un message). - Les routes : un
GET /chat, unPOST /sendMessageet unDELETE /message/{message}(le Route Model Binding vous donne directement le message à supprimer). - La vue, avec un formulaire (
<flux:input>et<flux:button>) et la liste. Pour la suppression, un formulaire avec@csrfet@method('DELETE').
Pour afficher l'heure, $message->created_at->format('d/m/Y H:i'). L'heure est décalée ? Laravel travaille en UTC par défaut : passez la clé timezone de config/app.php à 'Europe/Paris'.
Une fois le chat en place, testez-le à plusieurs.
Point de contrôle
Connectez-vous avec deux comptes dans deux navigateurs (ou une fenêtre de navigation privée). Les messages de l'un apparaissent chez l'autre… après avoir rechargé la page. Le compte administrateur voit les liens « Supprimer », l'autre non.
Faire évoluer le code pour utiliser de l'Ajax
Un instant !
Avant d'attaquer cette partie, nous allons réaliser ce mini TP ici pour comprendre comment fonctionne l'Ajax.
Votre chat fonctionne, mais il ne se met pas à jour automatiquement. Pour cela, vous allez utiliser de l'Ajax pour récupérer régulièrement les messages. Nous allons découper notre code en plusieurs parties :
- Une page
/getMessagesqui retourne uniquement la liste des messages, en HTML (sans layout). - La page
/chatqui affiche cette liste et la rafraîchit régulièrement grâce à un peu de JavaScript.
Une vue partielle
En général, pour récupérer des données en Ajax, on utilise une API Rest qui retourne du JSON. Ici, nous allons faire autrement : nous allons retourner du HTML directement. C'est une approche très « Web centric » qui simplifie énormément le code JavaScript.
Pour ne pas écrire deux fois le HTML d'un message, sortez la boucle d'affichage des messages dans une vue à part, par exemple resources/views/chat/messages.blade.php (sans <x-layouts::app>, juste la liste). Vous l'utiliserez à deux endroits :
- Dans la page
/chat, avec@include('chat.messages'). - Dans la méthode
getMessagesdu contrôleur, qui retourneview('chat.messages', [...]).
Pour le JavaScript
Grâce à l'Ajax, nous allons récupérer ce code HTML et le placer dans notre page /chat, par exemple toutes les 15 secondes. Voici un exemple de code JavaScript :
function getMessages() {
fetch('/getMessages')
.then(response => response.text())
.then(html => {
document.getElementById('messages').innerHTML = html;
});
}
setInterval(() => {
getMessages();
}, 15000);
document.addEventListener('DOMContentLoaded', function () {
getMessages();
});Quelques explications :
setIntervalexécute une fonction toutes les 15 secondes.fetchfait une requête HTTP pour récupérer les messages.response.text()récupère le contenu de la réponse en tant que texte.document.getElementById('messages').innerHTML = html;remplace le contenu de la balise d'identifiantmessagespar le HTML reçu.DOMContentLoadedpermet de récupérer les messages une première fois lorsque la page est chargée.
Extrait du template chat/index.blade.php :
<div id="messages">
Chargement des messages...
<!-- Les messages vont remplacer ce texte automatiquement -->
</div>L'approche HTMX
HTMX est une librairie JavaScript qui permet de faire des requêtes HTTP pour récupérer du HTML et l'ajouter à la page, sans écrire de JavaScript. C'est une approche très simple et très efficace pour faire des applications Web modernes.
Pour les curieux, je vous invite à consulter le site officiel de HTMX, ou mon TP sur le sujet ici.
Pour utiliser HTMX dans votre projet, ajoutez la librairie via un CDN, par exemple à la fin de votre vue chat/index.blade.php (à l'intérieur de <x-layouts::app>) :
<script src="https://unpkg.com/htmx.org@2.0.11"></script>Puis, utilisez HTMX pour charger et rafraîchir les messages, ici toutes les 2 secondes :
<div id="messages" hx-get="/getMessages" hx-trigger="every 2s">
@include('chat.messages')
</div>hx-get="/getMessages"fait une requête HTTP GET pour récupérer les messages.hx-trigger="every 2s"déclenche cette requête toutes les 2 secondes.- Par défaut, HTMX remplace le contenu de la balise par la réponse.
Ici, les messages sont affichés dès le chargement grâce au @include, puis HTMX prend le relais toutes les 2 secondes. Et comme le script est dans votre vue, il est bien chargé même quand vous arrivez sur le chat par un lien wire:navigate du menu.
Et avec Livewire ?
Le kit est construit avec Livewire, qui sait faire exactement la même chose avec l'attribut wire:poll.2s sur un composant. HTMX a l'avantage de fonctionner dans n'importe quel projet, même sans Livewire. Les deux approches partagent la même idée : le serveur reste maître du HTML.
Bonus : et si vous faisiez l'ajout en Ajax ?
Pour aller plus loin, vous pouvez également envoyer le formulaire de nouveau message en Ajax avec HTMX :
<form method="POST" action="/sendMessage" class="mt-6 flex gap-2"
hx-post="/sendMessage" hx-target="#messages" hx-swap="innerHTML"
hx-on::after-request="this.reset()">
@csrf
<flux:input name="message" placeholder="Votre message" required class="flex-1" />
<flux:button type="submit" variant="primary">Envoyer</flux:button>
</form>Ce formulaire envoie le message en Ajax à la route /sendMessage, puis remplace le contenu de la balise #messages par la réponse. La réponse doit donc être… la liste des messages en HTML (votre vue partielle) ! Et hx-on::after-request vide le champ après l'envoi.
Dans la méthode store, HTMX ajoute l'en-tête HX-Request à ses requêtes : vous pouvez ainsi retourner la vue partielle pour HTMX, et une redirection classique sinon (le formulaire fonctionne alors aussi sans JavaScript).
if ($request->header('HX-Request')) {
return view('chat.messages', [/* ... */]);
}
return redirect()->route('chat');Pour ma part, voici le résultat avec un compte administrateur :

C'est à vous de jouer !
Ici, l'idée c'est de vous laisser faire. Vous avez toutes les informations nécessaires pour ajouter un chat à votre application.
La double authentification
Pour terminer, un cadeau du kit : la double authentification (2FA) est déjà en place. Vous l'avez peut-être codée à la main dans le TP bonus La double authentification (2FA), avec un code envoyé par email. Le kit utilise une autre méthode, celle que vous avez sûrement déjà croisée sur GitHub, Google ou votre banque : une application d'authentification sur votre téléphone.
Le principe : TOTP
TOTP signifie Time-based One-Time Password (mot de passe à usage unique basé sur l'heure).
- À l'activation, le serveur génère un secret et vous le transmet une seule fois, sous forme de QR code.
- L'application du téléphone enregistre ce secret.
- À chaque connexion, le téléphone et le serveur calculent un code à partir du secret et de l'heure actuelle. Même secret, même heure : même code.
Le code change toutes les 30 secondes, et rien ne transite entre le serveur et le téléphone au moment de la connexion : pas d'email, pas de SMS, ça fonctionne même en mode avion.
Il vous faut une application d'authentification sur votre téléphone. N'importe laquelle compatible TOTP fait l'affaire : Google Authenticator, Microsoft Authenticator, ou des applications libres comme Aegis (Android) ou 2FAS (Android et iOS).
Pas de téléphone sous la main ?
Pas de panique. Vous pouvez saisir la clé à la main dans une application de bureau (par exemple KeePassXC, qui sait générer des codes TOTP), ou, pour vos tests uniquement, demander le code courant à Laravel avec Tinker :
php artisan tinker --execute='echo app(\PragmaRX\Google2FA\Google2FA::class)->getCurrentOtp(decrypt(\App\Models\User::where("email", "votre@email.fr")->first()->two_factor_secret));'C'est exactement le calcul que fait le téléphone. Évidemment, cette commande n'a de sens que sur votre poste de développement.
Activer la 2FA
Ouvrez Settings, puis l'onglet Security. Première surprise : le kit vous redemande votre mot de passe.

C'est voulu : activer ou désactiver la 2FA est une action sensible. Imaginez que vous laissiez votre session ouverte sur un poste du CDI : quelqu'un pourrait désactiver votre 2FA. La route settings/security (dans routes/settings.php) est donc protégée par le middleware password.confirm, qui exige un mot de passe retapé récemment (dans les 3 dernières heures).
Cliquez ensuite sur Enable 2FA : un QR code s'affiche.

Scannez-le avec votre application, cliquez sur Continue et saisissez le code à 6 chiffres affiché par votre téléphone. La 2FA est active ! Cliquez sur View recovery codes :

Que se passe-t-il si vous perdez votre téléphone ? Sans précaution, vous êtes enfermé dehors. C'est le rôle des codes de récupération : 8 codes, chacun utilisable une seule fois à la place du code TOTP. On les range en lieu sûr (sur papier, dans un gestionnaire de mots de passe), mais pas sur le téléphone.
Déconnectez-vous, puis reconnectez-vous : après le mot de passe, un nouvel écran vous demande le code.

Point de contrôle
- Un code valide vous amène sur le dashboard, un mauvais code affiche une erreur.
- Le lien « login using a recovery code » permet d'utiliser un code de récupération. Il ne fonctionne qu'une fois : réessayez-le à la connexion suivante, il est refusé.
- Le même code TOTP ne peut pas servir deux fois non plus : attendez le suivant (30 secondes) pour vous reconnecter.
Que se passe-t-il derrière ?
Vous n'avez écrit aucune ligne, mais vous savez maintenant où chercher. Je vous laisse retrouver chacune de ces briques dans votre projet :
config/fortify.php: la fonctionnalitéFeatures::twoFactorAuthentication(), avec ses deux options.confirmoblige à saisir un premier code avant que la 2FA ne soit active (un QR code mal scanné ne vous enferme donc pas dehors),confirmPasswordexige la confirmation du mot de passe.database/migrations/…_add_two_factor_columns_to_users_table.php: trois colonnes dansusers:two_factor_secret,two_factor_recovery_codesettwo_factor_confirmed_at.app/Models/User.php: le traitTwoFactorAuthenticatable. Un trait, c'est un morceau de classe « copié » dans la vôtre : votreUsergagne d'un coup les méthodeshasEnabledTwoFactorAuthentication(),twoFactorQrCodeSvg(),recoveryCodes()…resources/views/pages/settings/⚡two-factor-setup-modal.blade.php: le composant Livewire de la fenêtre d'activation. Regardez lesuseen haut du fichier : il appelle directement les actions de FortifyEnableTwoFactorAuthenticationetConfirmTwoFactorAuthentication. Le kit ne fait que l'interface, Fortify fait le travail.app/Providers/FortifyServiceProvider.php: la vue de l'écran de vérification (Fortify::twoFactorChallengeView) et sa limite de tentatives (RateLimiter::for('two-factor', …): 5 essais par minute).
Questions :
- Entre le mot de passe et le code, l'utilisateur est-il connecté ? Essayez d'ouvrir
/dashboardà ce moment-là. - Ouvrez votre base de données et regardez la colonne
two_factor_secretde votre utilisateur. Elle commence pareyJpdiI6…: le secret est chiffré. Mais le mot de passe, lui, est hashé. Pourquoi ne pas avoir hashé le secret aussi ?
Voir les réponses
- Non :
/dashboardvous renvoie sur/login. Fortify a vérifié le mot de passe, mais il a seulement mémorisé en session l'identifiant de l'utilisateur à vérifier. La connexion (Auth::login()) n'a lieu qu'après un code valide : tant que le second facteur n'est pas validé, la session n'est pas ouverte. - Un hash est à sens unique : on ne peut jamais retrouver la valeur d'origine, on peut seulement comparer. C'est parfait pour un mot de passe. Le secret TOTP, lui, doit être réutilisé : le serveur en a besoin à chaque connexion pour calculer le code attendu. Il est donc chiffré (réversible) avec la clé
APP_KEYde votre.env. Une copie de la base volée seule ne suffit pas à retrouver les secrets… à condition que laAPP_KEYne fuite pas avec !
Si vous avez fait le TP bonus sur la 2FA, une dernière question pour comparer les deux approches.
Question :
- Quels sont les avantages du TOTP par rapport au code envoyé par email ? Et ses inconvénients ?
Voir la réponse
Avantages du TOTP :
- Pas d'envoi : pas de serveur mail à configurer, pas d'email qui arrive en retard ou dans les spams, fonctionne sans réseau.
- Le facteur « ce que je possède » est vraiment le téléphone : avec le code par email, quelqu'un qui a volé le mot de passe de la boîte mail (souvent le même !) a les deux facteurs.
- Rien à stocker à chaque connexion : pas de code temporaire en base, pas d'expiration à gérer.
Inconvénients :
- Il faut installer une application, et l'activation demande une étape de plus (QR code, confirmation).
- La perte du téléphone impose de prévoir les codes de récupération.
Dans les deux cas, les règles de sécurité sont les mêmes : expiration courte, usage unique, limitation des tentatives, session ouverte seulement après validation.
Conclusion
Dans ce TP, vous avez :
- Créé un projet avec un starter kit (
laravel new), et compris le rôle de Fortify, le moteur qui tourne derrière. - Retrouvé dans le code généré les briques codées à la main (hash, session, middleware, limitation des tentatives).
- Découvert Tailwind et Vite (
composer run devpendant le développement,npm run buildpour figer). - Utilisé et créé des composants Blade (
<x-layouts::app>,<x-card>, les composants Flux), et transformé un composant HyperUI en<x-stat>réutilisable. - Protégé des pages avec le middleware
auth. - Modifié le code généré : nouveaux champs dans l'action
CreateNewUser, table liée (hasOne/belongsTo), Route Model Binding. - Mis en place des rôles avec une table pivot (
belongsToMany,attach,sync). - Rafraîchi une page en Ajax, avec
fetchou avec HTMX. - Utilisé et compris une double authentification TOTP (QR code, codes de récupération,
password.confirm).
N'oubliez pas de commiter votre projet !
Le kit vous a également généré un système de « mot de passe oublié » complet. Et vous savez exactement ce qu'il fait : c'est le mécanisme que vous avez codé à la main dans le TP Le reset de mot de passe (token, expiration, envoi d'email). Testez-le : avec MAIL_MAILER=log dans votre .env, l'email n'est pas envoyé mais écrit dans storage/logs/laravel.log, lien de reset compris.
Oui, nous avions tout codé à la main, mais le kit et Fortify le font automatiquement : la force d'un framework, c'est de ne pas tout recoder. Vous l'utiliserez désormais en sachant ce qu'il fait.
Le code entier, mais uniquement après avoir essayé
Vous souhaitez comparer avec mon code ? Téléchargez le projet de référence et décompressez-le à côté de votre projet (pas dedans !). Il contient l'ensemble du TP, chat compris. Comme pour tout projet Laravel récupéré, il faut l'installer avant de le lancer :
cd laravel-starter-kit
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
npm install
npm run build
composer run devSi migrate vous demande de créer le fichier database.sqlite, répondez yes. Le --seed crée les rôles admin et user : créez ensuite votre compte sur /register.
Pour la suite :
- Tester son application avec PHPUnit : le kit vous a fourni une trentaine de tests, vous allez apprendre à les lire et à écrire les vôtres.
- Eloquent les modèles simplement : aller plus loin avec les modèles.