Cours Vibe coding avec l'IA · Module 9: Clone de Discord
Authentification - NextAuth.js
Dans cette leçon4
Aujourd'hui, n'importe qui peut ouvrir le panneau de la guilde et changer les rôles : l'appli ignore qui est derrière l'écran. Les rôles des leçons précédentes disent ce que chacun a le droit de faire, mais il faut d'abord établir qui est qui. C'est le rôle de la connexion, ou authentification. Un système de mots de passe fait maison se rate facilement, c'est pourquoi la documentation de Next.js recommande des bibliothèques toutes prêtes comme NextAuth.js.
L'installation
Tu installes le paquet avec une seule commande :
1npm install next-authCette commande installe la version stable 4 de la bibliothèque, et c'est son API que montre cette leçon.
La connexion avec Google
La configuration va dans un Route Handler qui gérera toutes les adresses commençant par /api/auth :
1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from 'next-auth';
3import GoogleProvider from 'next-auth/providers/google';
4
5const handler = NextAuth({
6 providers: [
7 GoogleProvider({
8 clientId: process.env.GOOGLE_CLIENT_ID!,
9 clientSecret: process.env.GOOGLE_CLIENT_SECRET!
10 })
11 ]
12});
13
14export { handler as GET, handler as POST };Le dossier [...nextauth] attrape toutes les adresses sous /api/auth : la page de connexion, la déconnexion et le retour depuis Google. On utilise GoogleProvider avec un clientId et un clientSecret, que tu obtiens dans Google Cloud Console et gardes dans des variables d'environnement, jamais dans le code. Dans la liste des adresses de redirection autorisées chez Google, tu indiques http://localhost:3000/api/auth/callback/google, et en production la même adresse avec ton domaine. La dernière ligne exporte la même fonction comme GET et POST, car la connexion utilise les deux méthodes.
Une page protégée
Seul un pirate connecté doit voir le panneau. Dans un Server Component, tu récupères la session côté serveur :
1import { getServerSession } from 'next-auth';
2
3export default async function Dashboard() {
4 const session = await getServerSession();
5
6 if (!session) {
7 redirect('/api/auth/signin');
8 }
9
10 return <div>Bienvenue {session.user.name} !</div>;
11}await getServerSession() renvoie la session ou null. S'il n'y a pas de session, redirect('/api/auth/signin') envoie vers la page de connexion intégrée de NextAuth. Un pirate connecté est accueilli avec le nom tiré de session.user.name.
Il manque à l'exemple un import, sans lequel redirect ne fonctionne pas :
1import { redirect } from 'next/navigation';Dans un vrai projet, tu passes aussi à getServerSession le même objet de configuration (authOptions) qu'à NextAuth, comme le recommande sa documentation. La session est vérifiée sur le serveur avant que quoi que ce soit n'atteigne le navigateur, donc un visiteur non connecté ne voit rien du panneau.
Ce qui change dans les versions récentes
NextAuth.js se développe aujourd'hui sous le nom d'Auth.js. La version 5 est toujours marquée bêta : la configuration passe dans un fichier auth.ts qui exporte auth, handlers, signIn et signOut, et au lieu de getServerSession tu appelles une seule fonction :
1const session = await auth();Depuis septembre 2025, Auth.js est maintenu par l'équipe de Better Auth, qui recommande sa bibliothèque Better Auth pour les nouveaux projets et promet aux applis existantes de nouveaux correctifs de sécurité. Dans Next.js 16, le fichier middleware.ts, souvent utilisé pour protéger plusieurs pages à la fois, s'appelle proxy.ts.
Conseil du Capitaine Barbe-Rouge : ne poste pas un seul garde à la passerelle. Une redirection dans le proxy est pratique, mais vérifie aussi la session là où tu lis et modifies les données, comme le conseille la documentation de Next.js.
À retenir : la connexion répond à la question de qui tu es, et les rôles à ce que tu as le droit de faire. Pirate's Cove s'est construit dans cet ordre : serveurs avec catégories et salons, salons vocaux avec présence enrichie, rôles avec modération, et enfin intégrations et connexion. L'éditeur ci-dessous contient le panneau des rôles de la leçon sur les permissions, précisément l'endroit que la connexion doit protéger. Ensuite, fais les exercices.
Code de cette leçon : App.jsx
1// Gestionnaire de rôles
2import { useState } from 'react';
3
4const roles = [
5 { id: 1, name: 'Captain', color: '#e74c3c', memberCount: 3, position: 4 },
6 { id: 2, name: 'First Mate', color: '#3498db', memberCount: 5, position: 3 },
7 { id: 3, name: 'Navigator', color: '#2ecc71', memberCount: 8, position: 2 },
8 { id: 4, name: 'Crew', color: '#95a5a6', memberCount: 45, position: 1 }
9];
10
11const permissions = [
12 'ADMINISTRATOR',
13 'MANAGE_SERVER',
14 'MANAGE_ROLES',
15 'MANAGE_CHANNELS',
16 'KICK_MEMBERS',
17 'BAN_MEMBERS',
18 'SEND_MESSAGES',
19 'MANAGE_MESSAGES'
20];
21
22export default function App() {
23 const [selectedRoleId, setSelectedRoleId] = useState(1);
24
25 const selectedRole = roles.find(r => r.id === selectedRoleId);
26
27 return (
28 <div className="flex h-screen bg-gray-800">
29 {/* Liste des rôles */}
30 <div className="w-64 bg-gray-900 p-4">
31 <div className="flex items-center justify-between mb-4">
32 <h3 className="font-bold text-white">Rôles</h3>
33 <button className="w-8 h-8 bg-green-600 hover:bg-green-700 rounded-full text-white">
34 +
35 </button>
36 </div>
37
38 <div className="space-y-1">
39 {roles.sort((a, b) => b.position - a.position).map(role => (
40 <button
41 key={role.id}
42 onClick={() => setSelectedRoleId(role.id)}
43 className={`w-full text-left px-3 py-2 rounded flex items-center justify-between ${
44 selectedRoleId === role.id ? 'bg-gray-700' : 'hover:bg-gray-800'
45 }`}
46 >
47 <div className="flex items-center gap-2 flex-1">
48 <div
49 className="w-3 h-3 rounded-full"
50 style={{ backgroundColor: role.color }}
51 />
52 <span className="text-white text-sm truncate">{role.name}</span>
53 </div>
54 <span className="text-xs text-gray-400">{role.memberCount}</span>
55 </button>
56 ))}
57 </div>
58 </div>
59
60 {/* Éditeur de rôle */}
61 {selectedRole && (
62 <div className="flex-1 p-6 overflow-y-auto">
63 <h2 className="text-2xl font-bold text-white mb-6">
64 Modifier le rôle : {selectedRole.name}
65 </h2>
66
67 {/* Affichage */}
68 <div className="bg-gray-700 rounded-lg p-4 mb-6">
69 <h3 className="font-bold text-white mb-3">Affichage</h3>
70
71 <div className="mb-4">
72 <label className="block text-sm text-gray-300 mb-2">Nom du rôle</label>
73 <input
74 type="text"
75 key={selectedRole.id}
76 defaultValue={selectedRole.name}
77 className="w-full bg-gray-900 text-white px-3 py-2 rounded"
78 />
79 </div>
80
81 <div className="mb-4">
82 <label className="block text-sm text-gray-300 mb-2">Couleur du rôle</label>
83 <div className="flex items-center gap-3">
84 <input
85 type="color"
86 key={selectedRole.id}
87 defaultValue={selectedRole.color}
88 className="w-16 h-10 rounded cursor-pointer"
89 />
90 <span className="text-gray-400 text-sm">{selectedRole.color}</span>
91 </div>
92 </div>
93
94 <label className="flex items-center gap-2 text-sm text-gray-300 mb-2">
95 <input type="checkbox" className="w-4 h-4" />
96 Afficher les membres du rôle séparément
97 </label>
98
99 <label className="flex items-center gap-2 text-sm text-gray-300">
100 <input type="checkbox" className="w-4 h-4" />
101 Autoriser tout le monde à @mentionner ce rôle
102 </label>
103 </div>
104
105 {/* Permissions */}
106 <div className="bg-gray-700 rounded-lg p-4">
107 <h3 className="font-bold text-white mb-3">Permissions</h3>
108
109 <div className="space-y-2">
110 {permissions.map(permission => (
111 <label
112 key={permission}
113 className="flex items-center gap-3 text-sm text-gray-300 hover:bg-gray-600 p-2 rounded cursor-pointer"
114 >
115 <input type="checkbox" className="w-4 h-4" />
116 <span>{permission.split('_').join(' ')}</span>
117 </label>
118 ))}
119 </div>
120 </div>
121
122 {/* Actions */}
123 <div className="mt-6 flex gap-3">
124 <button className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-semibold">
125 Enregistrer les modifications
126 </button>
127 <button className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded font-semibold">
128 Supprimer le rôle
129 </button>
130 </div>
131 </div>
132 )}
133 </div>
134 );
135}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel fournisseur est utilisé dans la configuration NextAuth de l'exemple ?
2. Que se passe-t-il dans Dashboard quand getServerSession() ne renvoie aucune session ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne l'accès au nom de l'utilisateur connecté : session.user.name
- Clique dans l'ordre
Clique pour construire la récupération de la session côté serveur : await getServerSession()
- Tri vertical
Ordonne les étapes de construction du clone de Discord dans ce module :