Créer et sécuriser les comptes utilisateurs

Définissez le modèle utilisateur, l’authentification et la validation des mots de passe.

5 min read

Créer et sécuriser les comptes utilisateurs

Dans cette leçon, tu vas apprendre à :

  • Définir et personnaliser le modèle User dans Strapi 5
  • Construire des formulaires d’inscription et de connexion avec Remix
  • Mettre en place l’authentification par JWT et la sécurisation de routes
  • Ajouter un endpoint personnalisé pour comparer des mots de passe

Ton Algomax : concret, passionné, tutoiement bienveillant.


1 Définir le modèle User dans Strapi 5

Commence par créer un Collection Type user dans Strapi. Voici la configuration de base, avec les champs clés pour la sécurité :

api/user/content-types/user/schema.json
1
{
2
"kind": "collectionType",
3
"collectionName": "up_users",
4
"info": {
5
"displayName": "User",
6
"singularName": "user",
7
"pluralName": "users"
8
},
9
"options": {
10
"timestamps": true
11
},
12
"attributes": {
13
"username": {
14
"type": "string",
15
"minLength": 3,
16
"unique": true,
17
"required": true
18
},
19
"email": {
20
"type": "email",
21
"minLength": 6,
22
"required": true
23
},
24
"password": {
25
"type": "password",
26
"minLength": 6,
27
"private": true
28
},
29
"confirmed": {
30
"type": "boolean",
31
"default": false
32
},
33
"blocked": {
34
"type": "boolean",
35
"default": false
36
},
37
"role": {
38
"type": "relation",
39
"relation": "manyToOne",
40
"target": "plugin::users-permissions.role"
41
}
42
}
43
}

2 Ajouter un contrôleur custom pour comparer les mots de passe

Pour vérifier dynamiquement si le mot de passe actuel d’un utilisateur est valide (p. ex. pour changer son mot de passe), crée un endpoint comparePasswords :

api/user/controllers/user.ts
1
import { factories } from "@strapi/strapi";
2
3
export default factories.createCoreController(
4
"plugin::users-permissions.user",
5
({ strapi }) => ({
6
async comparePasswords(ctx) {
7
const { email, currentPassword } = ctx.request.body;
8
if (!email) {
9
throw new Error("We need an email to use this API");
10
}
11
if (!currentPassword || currentPassword.length < 6) {
12
throw new Error("Password must have at least 6 characters");
13
}
14
15
const user = await strapi.db
16
.query("plugin::users-permissions.user")
17
.findOne({ where: { email } });
18
19
const valid = await strapi
20
.plugin("users-permissions")
21
.service("user")
22
.validatePassword(currentPassword, user?.password);
23
24
return { isPasswordValid: valid };
25
},
26
})
27
);

3 Intégrer l’inscription et la connexion dans Remix

3.1 Route d’inscription (signup)

app/routes/signup.tsx
1
import { Form, useActionData, redirect } from "remix";
2
import { z } from "zod";
3
import { createUser } from "~/utils/user.server";
4
5
export const action = async ({ request }) => {
6
const form = await request.formData();
7
const data = {
8
username: form.get("username"),
9
email: form.get("email"),
10
password: form.get("password"),
11
};
12
// Valide avec Zod
13
const schema = z.object({
14
username: z.string().min(3),
15
email: z.string().email(),
16
password: z.string().min(6),
17
});
18
const result = schema.safeParse(data);
19
if (!result.success) {
20
return { errors: result.error.flatten().fieldErrors };
21
}
22
// Appelle Strapi
23
await createUser(result.data);
24
return redirect("/login");
25
};
26
27
export default function Signup() {
28
const actionData = useActionData();
29
return (
30
<Form method="post">
31
<input name="username" placeholder="Username" />
32
<input name="email" type="email" placeholder="Email" />
33
<input name="password" type="password" placeholder="Password" />
34
<button type="submit">Inscription</button>
35
{actionData?.errors && <div>{JSON.stringify(actionData.errors)}</div>}
36
</Form>
37
);
38
}
utils/user.server.ts
1
import { fetchAPI } from "./fetchAPI";
2
3
export async function createUser(data: {
4
username: string;
5
email: string;
6
password: string;
7
}) {
8
return fetchAPI("/api/auth/local/register", {
9
method: "POST",
10
body: JSON.stringify(data),
11
});
12
}

3.2 Route de connexion (login)

app/routes/login.tsx
1
import { Form, useActionData, redirect } from "remix";
2
import { loginUser } from "~/utils/user.server";
3
4
export const action = async ({ request }) => {
5
const form = await request.formData();
6
const identifier = form.get("email");
7
const password = form.get("password");
8
try {
9
const { jwt } = await loginUser({ identifier, password });
10
// Stocker le JWT dans un cookie sécurisé
11
return redirect("/", {
12
headers: {
13
"Set-Cookie": `jwt=${jwt}; HttpOnly; Path=/; Max-Age=2592000`,
14
},
15
});
16
} catch {
17
return { error: "Identifiants incorrects" };
18
}
19
};
20
21
export default function Login() {
22
const actionData = useActionData();
23
return (
24
<Form method="post">
25
<input name="email" type="email" placeholder="Email" />
26
<input name="password" type="password" placeholder="Password" />
27
<button type="submit">Connexion</button>
28
{actionData?.error && <div>{actionData.error}</div>}
29
</Form>
30
);
31
}

4 Protéger les routes avec Remix loaders

Pour sécuriser une page (profil, panier…), on vérifie le JWT dans le loader :

app/utils/session.server.ts
1
import { parse } from "cookie";
2
import { getUserByJwt } from "./user.server";
3
4
export async function requireUser(request: Request) {
5
const cookie = request.headers.get("Cookie") || "";
6
const { jwt } = parse(cookie);
7
if (!jwt) throw redirect("/login");
8
const user = await getUserByJwt(jwt);
9
if (!user) throw redirect("/login");
10
return user;
11
}
app/routes/profile.tsx
1
import { LoaderFunction } from "remix";
2
import { requireUser } from "~/utils/session.server";
3
4
export const loader: LoaderFunction = async ({ request }) => {
5
const user = await requireUser(request);
6
return user;
7
};
8
9
export default function Profile({ data }) {
10
return (
11
<div>
12
<h2>Bienvenue {data.username}</h2>
13
<p>Email : {data.email}</p>
14
</div>
15
);
16
}

5 Bonnes pratiques et points clés

  • Toujours hash et salter les mots de passe (Strapi le gère automatiquement).
  • Utiliser HttpOnly cookies pour stocker le JWT et le rendre inaccessible au JS.
  • Valider les inputs avec Zod ou une autre librairie de validation.
  • Personnaliser les contrôleurs Strapi pour ajouter des endpoints (ex.
    comparaison de mot de passe).
  • Protéger systématiquement les loaders dans Remix avec une fonction
    requireUser.

Exercices rapides

  1. Ajouter la confirmation par email

    • Dans Strapi, active le plugin Users & Permissions et configure le
      email-confirmation.
    • Ajoute le champ confirmationToken dans le modèle et gère la route
      /auth/confirm-email.
  2. Implémenter la réinitialisation de mot de passe

    • Crée un controller Strapi pour générer un resetPasswordToken.
    • Conçois un formulaire Remix pour saisir l’email, recevoir le lien, puis
      choisir un nouveau mot de passe.
  3. Renouvellement automatique du JWT

    • Avant chaque requête, vérifie la date d’expiration du token en front.
    • Si proche de l’expiration, appelle un endpoint /refresh-token dans Strapi
      (à créer) et mets à jour le cookie.

Liens utiles :