Cours Vibe coding avec l'IA · Module 9: Clone de Discord

Authentification - NextAuth.js

4 min de lecture
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-auth

Cette 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. 1. Quel fournisseur est utilisé dans la configuration NextAuth de l'exemple ?

  2. 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 :

Articles utiles