Créez une solution d'authentification robuste pour le serveur Node js en utilisant Knex pour la gestion de base de données, Redis pour une mise en cache efficace et Express pour un routage transparent. Dans ma quête d'une solution d'authentification rapide, intuitive et rationalisée pour mes applications Node.js, j'ai rencontré des scénarios exigeant une mise en œuvre rapide sans compromettre les fonctionnalités. De l'inscription et de la connexion des utilisateurs à la gestion des mots de passe oubliés, en passant par la mise à jour des données utilisateur et même la suppression de comptes, j'ai recherché une solution complète qui navigue de manière transparente à travers ces interactions utilisateur essentielles. Ainsi, mon article vise précisément à présenter cela : une approche cohérente intégrant des méthodologies claires pour mettre en œuvre l'authentification et la mise en cache, garantissant un flux d'utilisateurs robuste et efficace. Ici, nous contournerons les procédures d'installation fondamentales et la création de modèles, en nous concentrant directement sur les subtilités de l'authentification et du flux des utilisateurs. Nous inclurons tous les liens nécessaires pour obtenir les fichiers de configuration tout au long de l'article, garantissant ainsi un accès transparent aux ressources nécessaires à la configuration. Outils Pour cette implémentation, nous utiliserons la version 20.11.1 de Node.js aux côtés de Knex, Express et Redis. De plus, nous utiliserons PostgreSQL comme base de données, qui sera conteneurisée et orchestrée à l'aide de Docker pour une gestion transparente. Le nom de notre application sera . Créons ce dossier et exécutons pour générer de base user-flow-boilerplate npm init -y package.json { "name": "user-flow-boilerplate", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } Package initial.json L'étape suivante consiste à ajouter les dépendances nécessaires : : dépendances npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator : Dépendances dev npm i -D @babel/core @babel/eslint-parser @babel/plugin-transform-class-properties @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript @faker-js/faker @types/bcrypt @types/body-parser @types/cors @types/express @types/jest @types/jsonwebtoken @types/node @types/node-cron @types/validator @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-jest cross-env eslint eslint-config-prettier eslint-plugin-prettier jest nodemon npm-run-all prettier ts-jest ts-loader ts-node tsconfig-paths tslint typescript webpack webpack-cli webpack-node-externals et ajoutez des scripts qui construiront et exécuteront notre application : "scripts": { "start": "NODE_ENV=production node dist/bundle.js", "build": "NODE_ENV=production webpack --config webpack.config.js", "dev": "cross-env NODE_ENV=development && npm-run-all -p dev:*", "dev:build": "webpack --config webpack.config.js --watch", "dev:start": "nodemon --watch dist --exec node dist/bundle.js", "test": "NODE_ENV=test jest --config ./jest.config.js", "lint": "eslint ./src -c .eslintrc.json" }, Pour assurer le bon lancement de notre application, il est essentiel de créer un dossier et d'y placer notre fichier de point d'entrée initial, . src index.ts require('dotenv').config(); import process from 'process'; import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 9999; app.use(bodyParser.json()); app.use(cors()); app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); (async () => { try { app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })(); Fichier de point d'entrée Pour le développement, nous avons besoin d'avoir des paramètres pour , , , , , . Tous ces fichiers que j'ai décrits dans l'article suivant : . typscript lint jest bable prettier nodemon Création d'un serveur Node.js avec Postgres et Knex sur Express Après avoir configuré tous les paramètres et créé le point d'entrée, l'exécution devrait lancer le serveur et vous devriez vous attendre à voir un résultat similaire à celui-ci : npm run dev ./src/index.ts 1.7 KiB [built] [code generated] external "dotenv" 42 bytes [built] [code generated] external "process" 42 bytes [built] [code generated] external "express" 42 bytes [built] [code generated] external "body-parser" 42 bytes [built] [code generated] external "cors" 42 bytes [built] [code generated] webpack 5.90.3 compiled successfully in 751 ms [nodemon] restarting due to changes... [nodemon] starting `node dist/bundle.js` Server is running on port 9999 Ensuite, accédez à où nous établirons une collection dédiée au test de nos points de terminaison. Dans la nouvelle collection, ajoutez une nouvelle requête , appuyez sur (sur Mac, mais les touches dépendent de votre système d'exploitation) et nommez-la . Facteur GET cmd + E health Ajoutez une entrée pour l'URL : . Pour , ajoutez une nouvelle variable que vous allez utiliser dans la collection : {{BASE_URI}}/health BASE_URI http://localhost:9999/api/v1 Ensuite, cliquez simplement sur le bouton « Envoyer » et vous devriez observer le corps de la réponse : { "message": "OK" } Base de données Avant d'aller de l'avant, il est crucial que notre base de données soit opérationnelle. Nous y parviendrons en le lançant avec . Pour accéder et gérer la base de données, vous pouvez utiliser diverses plateformes de développement telles que . docker-compose pgAdmin Personnellement, je préfère utiliser , qui est équipé d'un pilote permettant une connectivité transparente aux bases de données PostgreSQL pour une gestion efficace. RubyMine Nous avons besoin d'un fichier avec les clés, mots de passe et noms de tests nécessaires : .env PORT=9999 WEB_HOST="localhost" # DB DB_HOST="localhost" DB_PORT=5432 DB_NAME="user_flow_boilerplate" DB_USER="username_123" DB_PASSWORD="SomeParole999" # User DEFAULT_PASSWORD="SomeParole999" JWT_SECRET="6f1d7e9b9ba56476ae2f4bdebf667d88eeee6e6c98c68f392ed39f7cf6e51c5a" # Test User TEST_EMAIL="test_email@test.com" TEST_USERNAME="test_username" TEST_PASSWORD="SomeParole999" # Redis REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD="SomeParole999" .env pour la connexion à la base de données, Redis et les valeurs de test pour les graines N'ayez crainte, j'ai généré aléatoirement le pour l'illustrer de manière plus authentique. Créons donc un fichier à la racine du projet : JWT_SECRET docker-compose.yml version: '3.6' volumes: data: services: database: build: context: . dockerfile: postgres.dockerfile image: postgres:latest container_name: postgres environment: TZ: Europe/Madrid POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} networks: - default volumes: - data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped redis: image: redis:latest container_name: redis command: redis-server --requirepass ${REDIS_PASSWORD} networks: - default ports: - "6379:6379" restart: unless-stopped fichier docker-compose avec services Nous allons lancer deux services dans Docker pour une connectivité rapide. J'ai rationalisé ce processus pour faciliter un accès rapide à la base de données ou à Redis, nous permettant ainsi de récupérer efficacement les données. Alors, exécutons ces services , et nous devons pouvoir voir la sortie après la sortie suivante : docker-compose up docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e4bef95de1dd postgres:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp postgres 365e3a68351a redis:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:6379->6379/tcp redis Maintenant, nous devons créer le fichier dans lequel nous stockons nos types pour l'application : src/@types/index.ts export enum Role { Admin = 'admin', Blogger = 'blogger', } export type UserSession = { id: number; }; export type DatabaseDate = { created_at: Date; updated_at: Date; }; export type DefaultUserData = { role: Role; }; export interface User extends DatabaseDate { id: number; email: string; username: string; password: string; role: Role; } Types de service À ce stade, vous devez avoir à la racine du dossier de projet et de base de données pour la connexion, les migrations et les graines. knexfile.ts J'ai laissé une explication assez détaillée dans l'article sur la façon de migrer et d'amorcer les utilisateurs vers la base de données où nous utilisons ces variables d'environnement. Création d'un serveur Node.js avec Postgres et Knex sur Express J'aimerais vérifier spécifiquement les migrations pour m'assurer que nous sommes sur la même longueur d'onde. Nous avons déjà lancé nos services, et nous devons pouvoir vérifier la connexion à la base de données. docker exec -it postgres psql -U username_123 user_flow_boilerplate Si la connexion est bonne, vous serez alors dans la console . Ok, si la connexion ne pose aucun problème, alors nous devrions pouvoir y migrer nos tables. Exécutez . Ensuite, vous devriez observer les colonnes nouvellement ajoutées dans votre table dans la base de données. psql knex migrate:latest users Semons-le avec de fausses données et vérifions à nouveau le tableau. knex seed:run Nous sommes donc désormais équipés pour manipuler la base de données, ce qui nous permet d'ajouter, de supprimer ou de mettre à jour des utilisateurs selon nos besoins. Routeur Enfin, nous pouvons oublier les paramètres et la préparation et nous concentrer spécifiquement sur le flux des utilisateurs. Pour cela, nous devons créer un routeur. Nous devons gérer par ce routeur les opérations suivantes : , , , , . login logout signup delete_user update_user Pour cela, sur , ajoutez le code suivant : src/routes/index.ts import { Router } from 'express'; import { authRouter } from 'src/routes/authRouter'; import { healthController } from 'src/controllers/healthController'; import { sessionController } from 'src/controllers/sessionController'; import { authMiddleware } from 'src/middlewares/authMiddleware'; import { userRouter } from 'src/routes/userRouter'; export const router = Router({ mergeParams: true }); router.get('/health', healthController); router.use('/auth', authRouter); router.get('/session', authMiddleware, sessionController); router.use('/user', authMiddleware, userRouter); router.use((_, res) => { return res.status(404).json({ message: 'Not Found' }); }); Fichier d'itinéraires Comme vous pouvez le voir, au début, nous avons ajouté route que nous avons déjà vérifié. Alors, mettons à jour le point d’entrée pour y appliquer ces routes. Tout d’abord, supprimez précédent. /health get -> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); et ajoutez en haut du fichier : import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router); et créez le premier contrôleur pour le contrôle avec le code : health src/controllers/healthController.ts import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok'); Contrôleur de santé Revenons maintenant au routeur et vérifions ce que nous devons ajouter de plus aux routes. Nous devons ajouter deux fichiers supplémentaires : et authRouter.ts userRouter.ts import { Router } from 'express'; import { signUpController } from 'src/controllers/auth/signUpController'; import { loginController } from 'src/controllers/auth/loginController'; export const authRouter = Router(); authRouter.post('/signup', signUpController); authRouter.post('/login', loginController); Routeur d'authentification import { Router } from 'express'; import { updateUserController } from 'src/controllers/user/updateUserController'; import { deleteUserController } from 'src/controllers/user/deleteUserController'; import { logoutController } from 'src/controllers/user/logoutController'; import { updatePasswordController } from 'src/controllers/user/updatePasswordController'; export const userRouter = Router(); userRouter.patch('/', updateUserController); userRouter.delete('/', deleteUserController); userRouter.post('/logout', logoutController); userRouter.post('/update-password', updatePasswordController); Routeur utilisateur J'ai divisé cette logique par souci de lisibilité et par souci de responsabilité de maintenir des fonctionnalités isolées. Toutes ces routes ont besoin de contrôleurs dont nous allons gérer la logique. Les routes d'authentification et de santé n'ont pas besoin d'un middleware d'authentification, donc ces routes ne sont pas protégées, mais s'il n'y a pas de correspondance, nous obtiendrons un statut 404. router.get('/health', healthController); router.use('/auth', authRouter); Maintenant que nous avons défini tous les itinéraires, nous devons définir le modèle utilisateur. Modèle utilisateur J'utiliserai un modèle de base pour le modèle utilisateur, à partir duquel je réutiliserai les méthodes CRUD. Bien que j'aie déjà abordé la création de modèles dans un autre , j'inclurai le modèle de base ici pour une meilleure visibilité et compréhension. Créer dans article src/models/Model.ts import { database } from 'root/database'; export abstract class Model { protected static tableName?: string; protected static get table() { if (!this.tableName) { throw new Error('The table name must be defined for the model.'); } return database(this.tableName); } public static async insert<Payload>(data: Payload): Promise<{ id: number; }> { const [result] = await this.table.insert(data).returning('id'); return result; } public static async updateOneById<Payload>( id: number, data: Payload ): Promise<{ id: number; }> { const [result] = await this.table.where({ id }).update(data).returning('id'); return result; } public static async delete(id: number): Promise<number> { return this.table.where({ id }).del(); } public static async findOneById<Result>(id: number): Promise<Result> { return this.table.where('id', id).first(); } public static async findOneBy<Payload, Result>(data: Payload): Promise<Result> { return this.table.where(data as string).first(); } } Modèle de base Avec le modèle de base, il faut pouvoir créer dans le même dossier : UserModel.ts import { Model } from 'src/models/Model'; import { Role, User, DefaultUserData } from 'src/@types'; export class UserModel extends Model { static tableName = 'users'; public static async create<Payload>(data: Payload) { return super.insert<Payload & DefaultUserData>({ ...data, role: data.role || Role.Blogger, }); } public static findByEmail(email: string): Promise<User | null> { return this.findOneBy< { email: string; }, User >({ email }); } public static findByUsername(username: string): Promise<User | null> { return this.findOneBy< { username: string; }, User >({ username }); } } Modèle utilisateur Dans le modèle d'utilisateur, je définis par défaut s'il n'est pas fourni par la charge utile. Et maintenant que nos modèles sont prêts, nous pouvons procéder à leur utilisation dans nos contrôleurs et middlewares. role Middleware d'authentification Le middleware d'authentification d'une application Node.js est responsable de l'authentification des demandes entrantes, garantissant qu'elles proviennent d'utilisateurs valides et autorisés. Il intercepte généralement les demandes entrantes, extrait les jetons d'authentification ou les informations d'identification et vérifie leur validité par rapport à un mécanisme d'authentification prédéfini, tel que JWT (JSON Web Tokens) dans ce cas. Si le processus d'authentification réussit, le middleware permet à la demande de passer au gestionnaire suivant dans le cycle demande-réponse. Cependant, si l'authentification échoue, il répond avec un code d'état HTTP approprié (par exemple, 401 Unauthorized) et fournit éventuellement un message d'erreur. Créez le dossier , et ajoutez-y un fichier avec le code suivant : src/middlewares authMiddleware.ts import { jwt } from 'src/utils/jwt'; import { Redis } from 'src/redis'; import type { Request, Response, NextFunction } from 'express'; import type { UserSession } from 'src/@types'; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; const JWT_SECRET = process.env.JWT_SECRET; if (!token) return res.sendStatus(401); if (!JWT_SECRET) { console.error('JWT_SECRET Not Found'); return res.sendStatus(500); } if (!token) return res.status(401).json({ error: 'Token not provided' }); try { const userSession = await jwt.verify<UserSession>(token); if (!userSession) { return res.sendStatus(401); } const storedToken = await Redis.getSession(userSession.id); if (!storedToken || storedToken !== token) { return res.sendStatus(401); } req.user = userSession; next(); } catch (error) { console.error('JWT_ERROR', error); return res.sendStatus(401); } } Fichier middleware d'authentification Le middleware d'authentification extrait le jeton JWT de l'en-tête de la requête, vérifie sa validité à l'aide de la bibliothèque JWT et vérifie si le jeton correspond à celui stocké dans Redis. Si le jeton est valide et correspond au jeton stocké, le middleware définit la session utilisateur authentifiée sur l'objet de requête ( ) et appelle la fonction pour passer le contrôle au prochain middleware ou gestionnaire de route. Sinon, il répond avec un code d'état 401 indiquant un échec d'authentification. req.user next() Jetons Web JSON Passons en revue l'utilitaire pour jwt. Créez dans le fichier avec le code suivant : src/utils/jwt.ts require('dotenv').config(); import jsonwebtoken from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET as string; export const jwt = { verify: <Result>(token: string): Promise<Result> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => { if (error) { reject(error); } else { resolve(decoded as Result); } }); }); }, sign: (payload: string | object | Buffer): Promise<string> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { try { resolve(jsonwebtoken.sign(payload, JWT_SECRET)); } catch (error) { reject(error); } }); }, }; Fichier utilitaire JWT Cet utilitaire joue un rôle essentiel dans la gestion des jetons Web JSON au sein de l'application Node.js. L'objet exporte des fonctions de signature et de vérification des JWT, en tirant parti de la bibliothèque . Ces fonctions facilitent la création et la validation des JWT, indispensables à la mise en œuvre des mécanismes d'authentification dans l'application. jwt jsonwebtoken L'utilitaire encapsule la fonctionnalité de gestion des JWT, garantissant des mécanismes d'authentification sécurisés au sein de l'application Node.js tout en adhérant aux meilleures pratiques en matière de gestion des variables d'environnement. Rédis Utilisé comme base de données, cache et courtier de messages. Couramment utilisé dans divers cas d'utilisation, notamment la mise en cache, la gestion de sessions, l'analyse en temps réel, les files d'attente de messagerie, les classements, etc. La vérification du jeton depuis Redis sert de couche supplémentaire de sécurité et de validation pour le jeton JWT. Passons aux paramètres. Pour cela, créez dans le fichier avec le code suivant : src/redis/index.ts require('dotenv').config({ path: '../../.env', }); import process from 'process'; import * as redis from 'redis'; const client = redis.createClient({ url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, }); client.on('error', error => console.error('Redis Client Error', error)); const connect = async () => { try { await client.connect(); console.log('Connected to Redis'); } catch (err) { console.error(`Could not connect to Redis: ${err}`); process.exit(1); } }; class Redis { public static setSession(userId: number, token: string) { if (!userId) throw new Error('userId is required'); if (!token) throw new Error('token is required'); try { return client.set(`session:${userId}`, token); } catch (error) { console.error(error); } } public static getSession(userId: number) { if (!userId) throw new Error('userId is required'); return client.get(`session:${userId}`); } public static deleteSession(userId: string) { if (!userId) throw new Error('userId is required'); try { return client.del(`session:${userId}`); } catch (error) { console.error(error); } } } export { client, connect, Redis }; Magasin de sessions Redis Par Redis, nous allons stocker et gérer les jetons de session utilisateur. Dans le middleware d'authentification, après avoir vérifié l'authenticité du jeton JWT, le middleware vérifie si le jeton existe et correspond à celui stocké dans Redis pour la session utilisateur correspondante. Cela permet de garantir que seuls les utilisateurs valides et autorisés peuvent accéder aux itinéraires protégés. Redis est utilisé comme magasin clé-valeur pour conserver les jetons de session utilisateur. Lorsqu'un utilisateur se connecte ou s'authentifie, son jeton de session est stocké dans Redis. Cela permet une récupération efficace et rapide des jetons de session lors des contrôles d'authentification ultérieurs. Redis est utilisé dans le middleware d'authentification pour une gestion efficace des sessions, tandis que le fichier lié à Redis gère la configuration et la connexion au serveur Redis et fournit des fonctions pour interagir avec Redis dans d'autres parties de l'application. Cette configuration garantit des mécanismes d'authentification sécurisés et fiables, avec des jetons de session utilisateur stockés et gérés dans Redis. La dernière partie est que nous devons nous connecter à Redis dans notre point d'entrée : // all imports import * as Redis from 'src/redis'; const app = express(); const PORT = process.env.PORT || 9999; // middlewares (async () => { try { await Redis.connect(); app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })(); Se connecter à Redis Après avoir terminé la préparation de l’authentification, nous pouvons maintenant nous concentrer sur les contrôleurs. Contrôleurs Les contrôleurs dans les routes aident à organiser la logique de l'application en séparant les préoccupations et en favorisant la maintenabilité du code. Nous avons déjà créé le contrôleur pour le contrôle de santé. Ensuite, nous procéderons à la création de contrôleurs pour gérer les opérations avec l'utilisateur. Le premier contrôleur que nous allons prendre est qui doit être dans avec le code suivant : sessionController.ts src/controllers import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const user = await UserModel.findOneById<User>(req.user.id); if (user) { return res.status(200).json(user); } else { return res.sendStatus(401); } } catch (error) { return res.sendStatus(500); } }; Contrôleur de session Ce contrôleur sert à gérer un point de terminaison lié à la session, probablement responsable de la récupération des informations sur l'utilisateur actuellement authentifié. Nous avons besoin de ce contrôleur pour les raisons suivantes : ce contrôleur permet à l'application de récupérer des informations sur la session de l'utilisateur, telles que son profil utilisateur ou d'autres données pertinentes. Ces informations peuvent être utiles pour personnaliser l'expérience utilisateur ou fournir un contenu personnalisé basé sur le profil de l'utilisateur. Informations sur la session utilisateur : en vérifiant si existe, le contrôleur garantit que seuls les utilisateurs authentifiés peuvent accéder au point de terminaison. Cela permet d'appliquer les règles d'authentification et d'autorisation, garantissant que les données utilisateur sensibles ne sont accessibles qu'aux utilisateurs autorisés. Authentification et autorisation : req.user le contrôleur interroge la base de données (à l'aide du ) pour récupérer les informations de l'utilisateur en fonction de son ID de session. Cela permet à l'application de récupérer dynamiquement des données spécifiques à l'utilisateur, offrant ainsi une expérience personnalisée à chaque utilisateur. Cette partie peut certainement être améliorée par le cache Redis : Récupération du profil utilisateur : UserModel import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const cachedProfile = await Redis.getSession(req.user.id); if (cachedProfile) { return res.status(200).json(JSON.parse(cachedProfile)); } else { const user = await UserModel.findOneById<User>(req.user.id); if (user) { await Redis.setSession(req.user.id, JSON.stringify(user), CACHE_EXPIRATION); return res.status(200).json(user); } else { return res.sendStatus(401); } } } catch (error) { console.error('Error retrieving user profile:', error); return res.sendStatus(500); } }; Fichier de contrôleur de session avec session définie par Redis Nous définissons une constante pour spécifier le délai d'expiration du cache en secondes. Dans cet exemple, il est défini sur 3 600 secondes (1 heure). Les données mises en cache sont périodiquement actualisées, empêchant ainsi les données obsolètes d'être transmises aux utilisateurs et préservant l'intégrité des données dans le cache. CACHE_EXPIRATION Avant de procéder à la création du , qui gère le processus d'inscription des nouveaux utilisateurs dans notre application, passons en revue le schéma : signUpController Dans notre cas, lorsque nous essayons de nous inscrire avec un e-mail existant dans la base de données, nous accordons la priorité à la confidentialité des utilisateurs en ne révélant pas explicitement si l'utilisateur existe. Au lieu de cela, nous informons le client avec un message générique indiquant . Invalid email or password Cette approche encourage le client à soumettre des informations d'identification valides sans divulguer d'informations inutiles sur les utilisateurs existants. Créons maintenant et ajoutons le code suivant : src/controllers/auth/signUpController.ts import bcrypt from 'bcrypt'; import { jwt } from 'src/utils/jwt'; import { Request, Response } from 'express'; import { validate } from 'src/helpers/validation/validate'; import { userSchema } from 'src/helpers/validation/schemas/userSchema'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; import { getRandomString } from 'src/utils/getRandomString'; type Payload = Omit<User, 'id' | 'created_at' | 'updated_at' | 'role'>; export async function signUpController(req: Request, res: Response) { const { email, password }: Payload = req.body; const validation = validate<Payload>(req.body, userSchema); if (!validation.isValid) { return res.status(400).send(`Invalid ${validation.invalidKey}`); } try { const user = await UserModel.findOneBy({ email }); if (user) { return res.status(400).json({ message: 'Invalid email or password' }); } const hashedPassword = (await bcrypt.hash(password, 10)) as string; const username = `${email.split('@')[0]}${getRandomString(5)}`; const createdUser = await UserModel.create<Payload>({ email, password: hashedPassword, username, }); const token = await jwt.sign({ id: createdUser.id, }); await Redis.setSession(createdUser.id, token); res.status(200).json({ token, }); } catch (error) { return res.sendStatus(500); } } Contrôleur d'inscription Le contrôleur reçoit une demande contenant l'adresse e-mail et le mot de passe de l'utilisateur, généralement à partir d'un formulaire d'inscription. Il valide les données entrantes par rapport à un prédéfini pour garantir qu'elles répondent au format requis. userSchema Si la validation réussit, indiquant aucun utilisateur existant et des champs valides, le contrôleur procède au hachage du mot de passe à l'aide de , génère un et crée l'utilisateur à l'aide . bcrypt.hash username UserModel.create Enfin, il génère un à l'aide , définit les données dans et renvoie le à l'utilisateur. token jwt session Redis token Concentrons-nous maintenant sur la création d'un contrôleur de connexion. Créez le fichier : src/controllers/auth/loginController.ts require('dotenv').config({ path: '../../.env', }); import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { jwt } from 'src/utils/jwt'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export async function loginController(req: Request, res: Response) { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: 'Invalid email or password' }); } try { const user = await UserModel.findByEmail(email); if (user) { const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: 'Invalid email or password' }); } const token: string = await jwt.sign({ id: user.id, }); await Redis.setSession(user.id, token); res.status(200).json({ token }); } else { return res.status(400).json({ message: 'Invalid email or password' }); } } catch (error) { console.error(error); return res.sendStatus(500); } } Contrôleur de connexion Essentiellement, nous commençons par valider les champs fournis puis vérifions l'existence d'un utilisateur. Si aucun utilisateur n'est trouvé, nous répondons avec un code d'état 400 accompagné du message , similaire au comportement du . Invalid email or password signupController Si un utilisateur existe, nous comparons le mot de passe fourni avec le mot de passe haché stocké dans la base de données à l'aide de . bcrypt.compare Si les mots de passe ne correspondent pas, nous répondons avec le message familier « E-mail ou mot de passe invalide ». Enfin, une fois l'authentification réussie, nous générons un jeton, définissons la session dans Redis et renvoyons le jeton au client. Passons en revue nos contrôleurs protégés, qui dépendent de la présence d'un user_id obtenu à partir du middleware. Nous nous appuyons systématiquement sur cet user_id pour les opérations au sein de ces contrôleurs. Dans les cas où la demande n'a pas d'en-tête , nous devons répondre avec un code d'état . authorization 401 const authHeader = req.headers['authorization']; Créez le fichier avec le code suivant : src/controllers/user/logoutController.ts import type { Request, Response } from 'express'; import { Redis } from 'src/redis'; export async function logoutController(req: Request, res: Response) { try { await Redis.deleteSession(req.user.id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } } Contrôleur de déconnexion Ce est responsable de la déconnexion d'un utilisateur du système. Dès réception d'une demande, il interagit avec le client Redis pour supprimer la session associée au . Si l'opération réussit, il répond avec un code d'état pour indiquer une déconnexion réussie. logoutController user.id 200 Cependant, si une erreur se produit pendant le processus, il répond avec un code d'état pour signaler une erreur interne du serveur. 500 Parlons ensuite de la suppression des données utilisateur. Créez et ajoutez ce code : src/controllers/user/deleteUserController.ts import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export const deleteUserController = async (req: Request, res: Response) => { const user_id = req.user.id; try { await Redis.deleteSession(user_id); await UserModel.delete(user_id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } }; Supprimer le contrôleur utilisateur Lorsqu'une requête est reçue, il extrait l'ID utilisateur de l'objet de requête, généralement obtenu à partir du middleware d'authentification. Par la suite, il procède à la suppression de la session associée à cet de Redis à l'aide du client Redis. Ensuite, il appelle la méthode du pour supprimer les données de l'utilisateur de la base de données. user_id delete UserModel En cas de suppression réussie de la session et des données utilisateur, il répond avec un code d'état pour indiquer une suppression réussie. En cas d'erreur lors du processus de suppression, il répond avec un code d'état pour signifier une erreur interne du serveur. 200 500 Pour mettre à jour les données utilisateur dans le système, créez et ajoutez le code suivant au fichier : src/controllers/user/updateUserController.ts import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { filterObject } from 'src/utils/filterObject'; type Payload = { first_name?: string; last_name?: string; username?: string; }; export const updateUserController = async (req: Request, res: Response) => { const { first_name, last_name, username } = req.body; const payload: Payload = filterObject({ first_name, last_name, username, }); try { const existingUserName = await UserModel.findByUsername(username); if (existingUserName) { return res.status(400).json({ error: 'Invalid username', }); } const updatedUser = await UserModel.updateOneById<typeof payload>(req.user.id, payload); res.status(200).json(updatedUser); } catch (error) { res.sendStatus(500); } }; Mettre à jour le contrôleur utilisateur Dès réception d'une requête, il extrait les champs , et du corps de la requête. Ensuite, il filtre ces champs à l'aide de la fonction utilitaire pour garantir que seuls les champs valides sont inclus dans la charge utile. first_name last_name username filterObject Par la suite, il vérifie si le fourni existe déjà dans la base de données. Si c'est le cas, le contrôleur répond avec un code d'état et un message d'erreur indiquant un non valide. Si le est unique, le contrôleur procède à la mise à jour des données utilisateur dans la base de données à l'aide de la méthode de . username 400 username username updateOneById UserModel Une fois la mise à jour réussie, il répond avec un code d'état et les données utilisateur mises à jour. En cas d'erreur pendant le processus de mise à jour, le contrôleur répond avec un code d'état pour signifier une erreur interne du serveur. 200 500 La dernière sera de mettre à jour le mot de passe, à peu près la même idée que la mise à jour des données utilisateur, mais avec hachage du nouveau mot de passe. Créez le dernier contrôleur de notre liste et ajoutez le code : src/controllers/user/updatePasswordController.ts import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import bcrypt from 'bcrypt'; export const updatePasswordController = async (req: Request, res: Response) => { try { const { password } = req.body; if (!password) return res.sendStatus(400); const hashedPassword = (await bcrypt.hash(password, 10)) as string; const user = await UserModel.updateOneById(req.user.id, { password: hashedPassword }); return res.status(200).json({ id: user.id }); } catch (error) { return res.sendStatus(500); } }; Mettre à jour le contrôleur de mot de passe Dès réception d'une demande, il extrait le nouveau mot de passe du corps de la demande. Il vérifie ensuite si un mot de passe est fourni dans le corps de la requête. Sinon, il répond avec un code d'état , indiquant une mauvaise demande. Ensuite, il hache le nouveau mot de passe à l'aide de la bibliothèque avec un facteur de sel de 10. 400 bcrypt Le mot de passe haché est ensuite stocké de manière sécurisée dans la base de données à l'aide de la méthode du , en l'associant au . Une fois la mise à jour réussie du mot de passe, le contrôleur répond avec un code d'état et un objet JSON contenant l'ID de l'utilisateur. updateOneById UserModel user.id 200 En cas d'erreur lors du processus de mise à jour du mot de passe, le contrôleur répond avec un code d'état pour indiquer une erreur interne du serveur comme dans les autres contrôleurs. 500 Assurez-vous de revoir et de configurer l'assistant de validation et les utilitaires à partir du . Une fois configuré, vous devriez être prêt à tester les points de terminaison. Dépôt GitHub Vérifions le point de terminaison de l'inscription : Comme évident, nous avons obtenu un jeton, qui sera utilisé dans l'en-tête pour récupérer la session. Nous avons envoyé le jeton d'autorisation dans l'en-tête au serveur et, en réponse, le serveur nous a fourni les données utilisateur extraites de la base de données. N'hésitez pas à explorer et expérimenter les fonctionnalités de sécurité et la mise en cache Redis. Une fois le modèle de base en place, vous pouvez accéder à des fonctionnalités supplémentaires, telles que la récupération de compte pour les utilisateurs qui oublient leur mot de passe. Ce sujet sera cependant réservé pour un prochain article. Conclusion La gestion du flux de routage et d’authentification des utilisateurs de manière évolutive peut s’avérer difficile. Bien que nous ayons mis en œuvre un middleware pour sauvegarder les itinéraires, il existe des stratégies supplémentaires disponibles pour améliorer les performances et la fiabilité du service. L'expérience utilisateur est encore améliorée grâce à la fourniture de messages d'erreur plus clairs, car la gestion des erreurs reste un aspect important qui nécessite une couverture plus complète. Cependant, nous avons mis en œuvre avec succès le flux d'authentification principal, permettant aux utilisateurs de s'inscrire, d'accéder à leurs comptes, de récupérer les données de session, de mettre à jour les informations utilisateur et de supprimer des comptes. J'espère que vous avez trouvé ce voyage instructif et que vous avez acquis des connaissances précieuses sur l'authentification des utilisateurs. Ressources Dépôt GitHub Knex.js Exprimer Créer une application de nœud Facteur Également publié ici