Skip to content

MaxVast/api-user-nestjs

Repository files navigation

User CRUD API - NestJS + MongoDB + JWT

API REST complète avec authentification JWT, gestion des utilisateurs et système de rôles, construite avec NestJS, MongoDB (Mongoose) et TypeScript.

📋 Table des matières


✨ Fonctionnalités

  • Authentification JWT (Access Token + Refresh Token)
  • Inscription et connexion des utilisateurs
  • Gestion des utilisateurs (CRUD complet)
  • Système de rôles (User, Admin)
  • Protection des routes par authentification
  • Autorisation basée sur les rôles (RBAC)
  • Protection ownership (un utilisateur peut modifier ses propres données)
  • Validation des données avec class-validator
  • Hashage des mots de passe avec bcrypt
  • Refresh token pour renouveler l'accès

🛠 Technologies utilisées

Technologie Version Usage
NestJS 10.x Framework backend TypeScript
TypeScript 5.x Langage de développement
MongoDB 7.x Base de données NoSQL
Mongoose 8.x ODM pour MongoDB
Passport 0.7.x Middleware d'authentification
JWT 10.x Tokens d'authentification
bcrypt 5.x Hashage des mots de passe
class-validator 0.14.x Validation des DTOs
class-transformer 0.5.x Transformation des données

🏗 Architecture

Structure du projet

src/
├── auth/                          # Module d'authentification
│   ├── decorators/               # Decorators personnalisés
│   │   ├── current-user.decorator.ts    # @CurrentUser()
│   │   ├── public.decorator.ts          # @Public()
│   │   └── roles.decorator.ts           # @Roles()
│   ├── dto/                      # Data Transfer Objects
│   │   ├── login.dto.ts         # DTO pour la connexion
│   │   ├── register.dto.ts      # DTO pour l'inscription
│   │   └── refresh-token.dto.ts # DTO pour le refresh
│   ├── guards/                   # Guards de protection
│   │   ├── jwt-auth.guard.ts           # Vérifie le JWT
│   │   ├── jwt-refresh-auth.guard.ts   # Vérifie le refresh token
│   │   ├── roles.guard.ts              # Vérifie les rôles
│   │   └── owner-or-admin.guard.ts     # Vérifie ownership ou admin
│   ├── strategies/               # Stratégies Passport
│   │   ├── jwt.strategy.ts            # Stratégie JWT
│   │   └── jwt-refresh.strategy.ts    # Stratégie refresh token
│   ├── auth.controller.ts       # Contrôleur (routes auth)
│   ├── auth.service.ts          # Service (logique métier)
│   └── auth.module.ts           # Module NestJS
│
├── users/                        # Module utilisateurs
│   ├── dto/                     # Data Transfer Objects
│   │   ├── create-user.dto.ts         # DTO création user
│   │   ├── update-user.dto.ts         # DTO mise à jour user
│   │   └── update-user-by-admin.dto.ts # DTO admin
│   ├── schemas/                 # Schémas Mongoose
│   │   └── user.schema.ts            # Schéma User
│   ├── users.controller.ts      # Contrôleur (routes users)
│   ├── users.service.ts         # Service (logique métier)
│   └── users.module.ts          # Module NestJS
│
├── app.module.ts                # Module racine
└── main.ts                      # Point d'entrée de l'application

📦 Installation

Prérequis

  • Node.js >= 22.x
  • npm ou yarn
  • MongoDB (local ou Atlas)

1. Installe les dépendances

npm install

2. Configure l'environnement

Crée un fichier .env (ou .env.local) à la racine :

# MongoDB
MONGODB_URI=mongodb://localhost:27017/user_crud
# Ou pour MongoDB Atlas :
# MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/user_crud

# JWT
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
JWT_EXPIRATION=1h
JWT_REFRESH_SECRET=your_super_secret_refresh_key
JWT_REFRESH_EXPIRATION=7d

# Application
PORT=3000
FRONTEND_URL=http://localhost:8080

⚠️ Important : Change les secrets JWT en production !

3. Lance MongoDB (si local)

# Linux/WSL
sudo systemctl start mongod

# macOS
brew services start mongodb-community

4. Lance l'application

# Mode développement (hot reload)
npm run start:dev

# Mode production
npm run build
npm run start:prod

L'API sera accessible sur http://localhost:3000


⚙️ Configuration

Variables d'environnement

Variable Description Exemple
MONGODB_URI URI de connexion MongoDB mongodb://localhost:27017/user_crud
JWT_SECRET Secret pour signer les access tokens my_secret_key
JWT_EXPIRATION Durée de vie des access tokens 1h, 15m, 7d
JWT_REFRESH_SECRET Secret pour signer les refresh tokens my_refresh_secret
JWT_REFRESH_EXPIRATION Durée de vie des refresh tokens 7d, 30d
PORT Port de l'application 3000
FRONTEND_URL URL du frontend (pour CORS) http://localhost:8080

Configuration MongoDB Atlas (Cloud)

  1. Crée un compte sur MongoDB Atlas
  2. Crée un cluster gratuit (M0)
  3. Configure Database Access (crée un utilisateur)
  4. Configure Network Access
  5. Récupère la connection string
  6. Remplace dans .env :
MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/user_crud?retryWrites=true&w=majority

🚀 Utilisation

Créer un utilisateur admin

Option 1 : Script MongoDB

# Connecte-toi à MongoDB
mongosh

# Utilise la base de données
use user_crud

# Crée un admin
db.users.insertOne({
  email: "admin@example.com",
  password: "$2b$10$...", // Hash de "AdminPass123" avec bcrypt
  firstName: "Super",
  lastName: "Admin",
  roles: ["admin", "user"],
  createdAt: new Date(),
  updatedAt: new Date()
})

Option 2 : Via l'API

  1. Inscris-toi normalement via /auth/register
  2. Modifie le rôle directement en DB :
mongosh
use user_crud
db.users.updateOne(
  { email: "ton-email@example.com" },
  { $set: { roles: ["admin", "user"] } }
)

📡 Routes API

🔓 Routes publiques (pas d'authentification requise)

Inscription

POST /auth/register
Content-Type: application/json

{
  "email": "john@example.com",
  "password": "password123",
  "firstName": "John",
  "lastName": "Doe"
}

Réponse (201) :

{
  "user": {
    "id": "65a1b2c3d4e5f6g7h8i9j0k1",
    "email": "john@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "roles": ["user"]
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Connexion

POST /auth/login
Content-Type: application/json

{
  "email": "john@example.com",
  "password": "password123"
}

Réponse (200) : Identique à /auth/register

Rafraîchir le token

POST /auth/refresh
Content-Type: application/json

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Réponse (200) :

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

🔒 Routes protégées (authentification requise)

Toutes les routes suivantes nécessitent un access token dans le header :

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Mon profil

GET /auth/me

Réponse (200) :

{
  "user": {
    "id": "65a1b2c3d4e5f6g7h8i9j0k1",
    "email": "john@example.com",
    "roles": ["user"]
  }
}

Déconnexion

POST /auth/logout

Réponse (200) :

{
  "message": "Déconnexion réussie"
}

👤 Routes utilisateurs

Lister tous les utilisateurs (admin seulement)

GET /users

Réponse (200) :

[
  {
    "id": "65a1b2c3d4e5f6g7h8i9j0k1",
    "email": "john@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "roles": ["user"],
    "createdAt": "2026-01-30T10:00:00.000Z",
    "updatedAt": "2026-01-30T10:00:00.000Z"
  }
]

Mon profil utilisateur

GET /users/me

Réponse (200) : Détails de l'utilisateur connecté

Détails d'un utilisateur (admin seulement)

GET /users/:id

Réponse (200) : Détails de l'utilisateur

Modifier un utilisateur (owner ou admin)

PATCH /users/:id
Content-Type: application/json

{
  "firstName": "Jane",
  "lastName": "Smith"
}

Réponse (200) : Utilisateur modifié

Restrictions :

  • Un utilisateur peut modifier ses propres données uniquement
  • Un admin peut modifier n'importe quel utilisateur
  • Un utilisateur normal ne peut pas modifier ses rôles

Supprimer un utilisateur (admin seulement)

DELETE /users/:id

Réponse (204) : Pas de contenu


🔐 Authentification et autorisation

Système de tokens JWT

L'API utilise 2 types de tokens JWT :

1. Access Token (courte durée : 1h)

  • Utilisé pour toutes les requêtes API
  • Envoyé dans le header Authorization: Bearer <token>
  • Contient : { sub: userId, email, roles }
  • Expire rapidement pour la sécurité

2. Refresh Token (longue durée : 7 jours)

  • Permet de renouveler l'access token sans se reconnecter
  • Stocké en base de données (hashé avec bcrypt)
  • Vérifié en DB à chaque utilisation
  • Révoqué lors de la déconnexion

Cycle de vie d'une session

1. User se connecte → Reçoit accessToken + refreshToken
2. User fait des requêtes avec accessToken
3. AccessToken expire (1h) → Erreur 401
4. Client envoie refreshToken à /auth/refresh
5. Serveur génère nouveaux tokens
6. User peut continuer à utiliser l'API

Structure d'un JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2NWExYjJjMyIsImVtYWlsIjoiam9obkBleGFtcGxlLmNvbSIsInJvbGVzIjpbInVzZXIiXSwiaWF0IjoxNzA2NjIyMDAwLCJleHAiOjE3MDY2MjU2MDB9.signature

└────────── Header ──────────┘ └──────────────────── Payload (données) ────────────────────────┘ └─ Signature ─┘

Décodage du payload :

{
  "sub": "65a1b2c3...",    // User ID
  "email": "john@example.com",
  "roles": ["user"],
  "iat": 1706622000,       // Issued At
  "exp": 1706625600        // Expiration
}

👮 Gestion des rôles

Rôles disponibles

Rôle Description Permissions
user Utilisateur standard Accès à son profil, modification de ses données
admin Administrateur Accès complet à tous les utilisateurs, gestion des rôles

Protections implémentées

1. Protection par authentification

Toutes les routes (sauf /auth/register et /auth/login) nécessitent un JWT valide.

2. Protection par rôle

Certaines routes sont réservées aux admins :

@Roles('admin')
@Get()
findAll() { ... }

3. Protection ownership

Un utilisateur peut modifier uniquement ses propres données :

@UseGuards(OwnerOrAdminGuard)
@Patch(':id')
update(@Param('id') id: string) { ... }

Le guard vérifie :

  • Si user.id === :id → ✅ Autorisé (c'est son profil)
  • OU si user.roles.includes('admin') → ✅ Autorisé (c'est un admin)
  • Sinon → ❌ 403 Forbidden

Tableau des permissions

Route Authentification Autorisation
POST /auth/register ❌ Publique Tous
POST /auth/login ❌ Publique Tous
POST /auth/refresh ✅ Refresh token Tous authentifiés
POST /auth/logout ✅ JWT Tous authentifiés
GET /auth/me ✅ JWT Tous authentifiés
GET /users ✅ JWT 👮 Admin seulement
GET /users/me ✅ JWT Tous authentifiés
GET /users/:id ✅ JWT 👮 Admin seulement
PATCH /users/:id ✅ JWT 👤 Owner OU 👮 Admin
DELETE /users/:id ✅ JWT 👮 Admin seulement

📊 Modèle de données

Schéma User (MongoDB/Mongoose)

{
  _id: ObjectId,              // ID MongoDB (auto-généré)
  email: String,              // Email unique, lowercase, trim
  password: String,           // Hash bcrypt (jamais en clair)
  firstName?: String,         // Prénom (optionnel)
  lastName?: String,          // Nom (optionnel)
  roles: String[],            // Tableau de rôles ['user', 'admin']
  refreshToken?: String,      // Refresh token hashé (optionnel)
  createdAt: Date,            // Date de création (auto)
  updatedAt: Date             // Date de mise à jour (auto)
}

Transformation JSON

Lors de la sérialisation (réponse API), le schéma est transformé :

// En base de données
{
  _id: ObjectId("65a1b2c3d4e5f6g7h8i9j0k1"),
  email: "john@example.com",
  password: "$2b$10$abcd...",
  refreshToken: "$2b$10$wxyz...",
  roles: ["user"],
  createdAt: ISODate("2026-01-30T10:00:00Z"),
  updatedAt: ISODate("2026-01-30T10:00:00Z")
}

// Réponse API (transformé)
{
  "id": "65a1b2c3d4e5f6g7h8i9j0k1",  // _id → id
  "email": "john@example.com",
  "roles": ["user"],
  "createdAt": "2026-01-30T10:00:00.000Z",
  "updatedAt": "2026-01-30T10:00:00.000Z"
  // password et refreshToken exclus automatiquement
}

Index MongoDB

Pour optimiser les performances :

// Index sur email (unique + recherche rapide)
UserSchema.index({ email: 1 });

🛡 Sécurité

Mesures de sécurité implémentées

1. Hashage des mots de passe

  • Utilisation de bcrypt avec un salt de 10 rounds
  • Les mots de passe ne sont jamais stockés en clair
  • Vérification sécurisée avec bcrypt.compare()
const hashedPassword = await bcrypt.hash(password, 10);
const isValid = await bcrypt.compare(plainPassword, hashedPassword);

2. Protection des tokens

  • Access token : expire en 1h (limite l'exposition)
  • Refresh token : stocké hashé en DB, révocable
  • Signature avec des secrets forts (JWT_SECRET)

3. Validation des données

  • Validation automatique avec class-validator
  • Whitelist activée (propriétés inconnues rejetées)
  • Transformation automatique des types
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,              // Retire les props non définies
  forbidNonWhitelisted: true,   // Erreur si props inconnues
  transform: true               // Transforme les types auto
}));

4. Exclusion des données sensibles

  • Le password n'est jamais retourné dans les réponses API
  • Le refreshToken est également exclu
  • Transformation automatique via @Exclude() et toJSON

5. CORS configuré

  • Seules les origines autorisées peuvent accéder à l'API
  • Configuration via FRONTEND_URL
app.enableCors({
  origin: process.env.FRONTEND_URL,
  credentials: true
});

Bonnes pratiques à suivre

En production :

  • Change les secrets JWT (JWT_SECRET, JWT_REFRESH_SECRET)
  • Utilise HTTPS uniquement
  • Active synchronize: false dans TypeORM/Mongoose
  • Configure des variables d'environnement sécurisées
  • Mets en place un rate limiting (limiteur de requêtes)
  • Active les logs d'audit
  • Utilise un reverse proxy (Nginx, Traefik)

Ne jamais :

  • Exposer les secrets dans le code source
  • Stocker des mots de passe en clair
  • Retourner des erreurs détaillées en production
  • Accepter synchronize: true en prod (risque de perte de données)

📚 Concepts clés NestJS

Modules

Les modules organisent l'application en blocs fonctionnels :

@Module({
  imports: [MongooseModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService]  // Rend le service disponible ailleurs
})
export class UsersModule {}

Controllers

Gèrent les routes HTTP et délèguent la logique aux services :

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}
  
  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

Services (Providers)

Contiennent la logique métier :

@Injectable()
export class UsersService {
  constructor(@InjectModel(User.name) private userModel: Model<User>) {}
  
  async findAll(): Promise<User[]> {
    return this.userModel.find().exec();
  }
}

Guards

Protègent les routes (authentification, autorisation) :

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// Utilisation
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile() { ... }

Decorators

Ajoutent des métadonnées ou extraient des données :

// Définition
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  }
);

// Utilisation
@Get('me')
getMe(@CurrentUser() user) {
  return user;
}

DTOs (Data Transfer Objects)

Définissent la structure et valident les données :

export class CreateUserDto {
  @IsEmail()
  email: string;
  
  @MinLength(8)
  password: string;
}

Strategies (Passport)

Gèrent les mécanismes d'authentification :

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  async validate(payload: JwtPayload) {
    // Charge et retourne l'utilisateur
    return { id: payload.sub, email: payload.email, roles: payload.roles };
  }
}

Dependency Injection

NestJS injecte automatiquement les dépendances :

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,  // ← Injecté auto
    private jwtService: JwtService       // ← Injecté auto
  ) {}
}

🎯 Comparaison avec Symfony

Si tu viens de Symfony, voici les équivalences :

Symfony NestJS
Bundle Module
Controller Controller
Service Service (Provider)
Entity Schema (Mongoose)
Repository Model (Mongoose)
FormType DTO
Validator class-validator
Security/Authenticator Strategy (Passport)
#[IsGranted()] @UseGuards(RolesGuard)
$this->getUser() @CurrentUser()
Security/Voter Custom Guard
EventDispatcher EventEmitter
DependencyInjection Dependency Injection

📝 Exemples de tests (Postman/cURL)

1. Inscription d'un utilisateur

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123",
    "firstName": "Test",
    "lastName": "User"
  }'

2. Connexion

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "password123"
  }'

Sauvegarde le token retourné dans une variable :

TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

3. Accéder à son profil

curl -X GET http://localhost:3000/auth/me \
  -H "Authorization: Bearer $TOKEN"

4. Modifier son profil

curl -X PATCH http://localhost:3000/users/<USER_ID> \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Nouveau Prénom"
  }'

5. Rafraîchir le token

REFRESH_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

curl -X POST http://localhost:3000/auth/refresh \
  -H "Content-Type: application/json" \
  -d "{\"refreshToken\": \"$REFRESH_TOKEN\"}"

6. Déconnexion

curl -X POST http://localhost:3000/auth/logout \
  -H "Authorization: Bearer $TOKEN"

🔧 Commandes utiles

# Développement
npm run start:dev          # Lance en mode watch (hot reload)
npm run start:debug        # Lance en mode debug

# Production
npm run build              # Compile TypeScript → JavaScript
npm run start:prod         # Lance la version compilée

# Génération de code
nest g module nom          # Crée un module
nest g controller nom      # Crée un contrôleur
nest g service nom         # Crée un service
nest g resource nom        # Crée module + controller + service + DTOs

# MongoDB
mongosh                    # Ouvre le shell MongoDB
use user_crud              # Sélectionne la DB
db.users.find()            # Liste tous les users
db.users.countDocuments()  # Compte les users

🐛 Troubleshooting

Erreur : "connect ECONNREFUSED 127.0.0.1:27017"

Cause : MongoDB n'est pas lancé

Solution :

sudo systemctl start mongod

Erreur : "JWT malformed" ou "invalid signature"

Cause : Token invalide ou secret JWT incorrect

Solution :

  • Vérifie que JWT_SECRET dans .env correspond
  • Assure-toi d'envoyer le bon token
  • Reconnecte-toi pour obtenir un nouveau token

Erreur : "email must be a valid email"

Cause : Validation DTO échouée

Solution :

  • Vérifie le format de l'email
  • Assure-toi d'envoyer tous les champs requis

Erreur : 403 Forbidden sur une route

Cause : Permissions insuffisantes

Solution :

  • Vérifie que ton user a le rôle requis (admin, etc.)
  • Pour les routes ownership, vérifie que tu modifies tes propres données

📖 Ressources


📄 Licence

Ce projet est à usage éducatif.


👨‍💻 Auteur

Développé dans le cadre de l'apprentissage de NestJS, TypeScript et MongoDB.


Prochaines étapes recommandées :

  • Protection contre l'escalade de privilèges
  • Ajouter la pagination et le filtrage
  • Implémenter l'upload de fichiers (photo de profil)
  • Ajouter la vérification par email
  • Créer un système de reset de mot de passe
  • Ajouter des tests
  • Générer la documentation Swagger/OpenAPI
  • Dockeriser l'application
  • Mettre en place un CI/CD

About

API REST complète avec authentification JWT, gestion des utilisateurs et système de rôles, construite avec NestJS, MongoDB (Mongoose) et TypeScript.

Topics

Resources

Stars

Watchers

Forks

Contributors