Formulaires d’inscription et de connexion

Crée des formulaires modernes, gère la validation et l’état avec Zod et useActionState.

5 min read

Accéder gratuitement à cette formation

Renseigne ton email pour débloquer immédiatement cette formation gratuite.

Dans ce chapitre du module Persistance et authentification avec Prisma, on va créer et sécuriser deux formulaires clés : l’inscription et la connexion. Tu y découvriras :

  • Comment structurer des React Server Components pour protéger l’accès.
  • L’utilisation des Server Actions (use server) pour traiter les formulaires côté serveur.
  • Le hook useActionState pour gérer l’état, les erreurs et le loader côté client.
  • Les fonctions Prisma pour créer et authentifier des utilisateurs.

Présentation générale

Next.js 15 introduit les Server Components et les Server Actions. Ici, les pages app/login et app/register sont des RSC (React Server Components). Elles vont importer un composant <LoginForm /> ou <RegisterForm /> qui, lui, est un Client Component et utilise useActionState pour piloter la soumission du formulaire.

Création de la page login

On commence par créer la route /login. Cette page est un RSC :
elle vérifie si l’utilisateur est déjà connecté et le redirige si besoin.

app/login/page.tsx
1
'use server';
2
3
import { redirect } from 'next/navigation';
4
import { getOptionalUser } from '../server/session';
5
import { LoginForm } from '../LoginForm';
6
7
export default async function Login() {
8
const user = await getOptionalUser();
9
if (user) {
10
redirect('/');
11
}
12
return <LoginForm />;
13
}

Composant LoginForm (client)

Le formulaire de connexion est un Client Component :
il utilise useActionState pour appeler l’action serveur login et afficher les erreurs.

app/LoginForm.tsx
1
'use client';
2
import { useActionState, useEffect, useRef } from 'react';
3
import { login } from './server/auth';
4
5
export function LoginForm() {
6
const [state, formAction, pending] = useActionState(login, {
7
errors: { email: undefined, password: undefined },
8
email: '',
9
password: '',
10
});
11
12
const emailRef = useRef<HTMLInputElement>(null);
13
const passwordRef = useRef<HTMLInputElement>(null);
14
15
useEffect(() => {
16
if (state?.errors?.email) {
17
emailRef.current?.focus();
18
} else if (state?.errors?.password) {
19
passwordRef.current?.focus();
20
}
21
}, [state?.errors]);
22
23
return (
24
<form action={formAction} className="space-y-4">
25
<div>
26
<label htmlFor="email">Email address</label>
27
<input
28
id="email"
29
name="email"
30
type="email"
31
ref={emailRef}
32
defaultValue={state.email}
33
required
34
className={`border ${
35
state.errors?.email ? 'border-red-500' : 'border-gray-300'
36
}`}
37
/>
38
{state.errors?.email && (
39
<p className="text-red-600">{state.errors.email.join(', ')}</p>
40
)}
41
</div>
42
43
<div>
44
<label htmlFor="password">Password</label>
45
<input
46
id="password"
47
name="password"
48
type="password"
49
ref={passwordRef}
50
defaultValue={state.password}
51
required
52
className={`border ${
53
state.errors?.password ? 'border-red-500' : 'border-gray-300'
54
}`}
55
/>
56
{state.errors?.password && (
57
<p className="text-red-600">{state.errors.password.join(', ')}</p>
58
)}
59
</div>
60
61
<button
62
type="submit"
63
disabled={pending}
64
className="bg-blue-600 text-white px-4 py-2 rounded"
65
>
66
{pending ? 'Signing in...' : 'Sign in'}
67
</button>
68
</form>
69
);
70
}

Ajout de la logique serveur pour login

Dans app/server/auth.ts, on définit la fonction login marquée avec "use server". Elle :

  1. Valide les champs avec Zod.
  2. Vérifie l’existence de l’utilisateur dans Prisma.
  3. Compare les mots de passe (bcryptjs).
  4. Crée un cookie via setUserId.
  5. Effectue la redirection.
app/server/auth.ts
1
'use server';
2
import { prisma } from './db';
3
import { compare } from 'bcryptjs';
4
import { redirect } from 'next/navigation';
5
import { z } from 'zod';
6
import { setUserId } from './session';
7
8
const LoginSchema = z.object({
9
email: z.string().email(),
10
password: z.string().min(6),
11
});
12
13
export async function login(prevState: unknown, formData: FormData) {
14
const email = formData.get('email') as string;
15
const password = formData.get('password') as string;
16
17
const result = LoginSchema.safeParse({ email, password });
18
if (!result.success) {
19
return { errors: result.error.flatten().fieldErrors, email, password };
20
}
21
22
const user = await prisma.user.findUnique({ where: { email } });
23
if (!user) {
24
return { errors: { email: ['User not found'] }, email, password };
25
}
26
27
const valid = await compare(password, user.password);
28
if (!valid) {
29
return { errors: { password: ['Invalid password'] }, email, password };
30
}
31
32
await setUserId({ userId: user.id });
33
redirect('/');
34
}

Gestion de la session

La fonction setUserId ajoute un cookie user_session.
Le Router et les Server Components peuvent ensuite lire cet ID pour afficher la navbar ou les pages protégées.

app/server/session.ts
1
'use server';
2
import { cookies } from 'next/headers';
3
4
export async function setUserId({ userId }: { userId: number }) {
5
cookies().set('user_session', String(userId), {
6
httpOnly: true,
7
secure: process.env.NODE_ENV === 'production',
8
path: '/',
9
maxAge: 60 * 60 * 24 * 30, // 30 jours
10
});
11
}

Création du formulaire d’inscription

On répète la même approche pour /registerRegisterFormregister (Server Action).

app/register/page.tsx
1
'use server';
2
3
import { redirect } from 'next/navigation';
4
import { getOptionalUser } from '../server/session';
5
import { RegisterForm } from '../RegisterForm';
6
7
export default async function Register() {
8
const user = await getOptionalUser();
9
if (user) redirect('/');
10
return <RegisterForm />;
11
}
app/RegisterForm.tsx
1
'use client';
2
import { useActionState, useEffect, useRef } from 'react';
3
import { register } from './server/auth';
4
5
export function RegisterForm() {
6
const [state, formAction, pending] = useActionState(register, {
7
errors: { email: undefined, password: undefined },
8
email: '',
9
password: '',
10
});
11
const emailRef = useRef<HTMLInputElement>(null);
12
const passwordRef = useRef<HTMLInputElement>(null);
13
14
useEffect(() => {
15
if (state?.errors?.email) emailRef.current?.focus();
16
else if (state?.errors?.password) passwordRef.current?.focus();
17
}, [state?.errors]);
18
19
return (
20
<form action={formAction} className="space-y-4">
21
{/* similaire à LoginForm, avec labels et validation */}
22
<button disabled={pending}>
23
{pending ? 'Signing up...' : 'Sign up'}
24
</button>
25
</form>
26
);
27
}
app/server/auth.ts
1
// …
2
export async function register(prevState: unknown, formData: FormData) {
3
'use server';
4
// validation Zod
5
// check email unique
6
// hash password avec bcryptjs
7
// prisma.user.create(...)
8
// setUserId + redirect('/')
9
}

Points clés à retenir

  • Les Server Components (pages) restent statiques côté client : tout hook se fait dans un Client Component.
  • useActionState facilite la gestion des erreurs et du loader.
  • Les Server Actions ('use server') peuvent accéder à Prisma et à cookies().
  • La validation avec Zod garantit la cohérence des données.
  • L’authentification repose sur un cookie HTTP-only : le cookie + Server Components protègent tes routes.

Exercices

  1. Ajouter un champ “username” au formulaire d’inscription et au modèle Prisma, puis rafraîchir la navbar pour afficher ce nom d’utilisateur.
  2. Protéger la page /profile en créant un Server Component app/profile/page.tsx qui affiche l’email et un lien de logout en Server Action.
  3. Implémenter un formulaire “forgot password” qui envoie un token par e-mail (mock).