Skip to content

Étape 4 — Ajout des API CRUD pour les étudiants

Objectif : exposer des endpoints create/read/update/delete pour la ressource Student, sécurisés par Bearer Token (JWT généré à l'étape 2).

Prérequis découvert en cours de route : le filtre JWT n'existait pas

SpringSecurityConfig protégeait déjà .anyRequest().authenticated(), mais aucun mécanisme ne lisait le header Authorization pour peupler le contexte de sécurité — la ligne était commentée :

// .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class)

Sans ce filtre, toute route protégée aurait renvoyé 401 même avec un JWT valide. Ajouté avant de commencer le CRUD :

  • JwtService : ajout de extractUsername(), isTokenValid(), isTokenExpired() (parsing via Jwts.parser().verifyWith(...)).
  • JwtAuthenticationFilter (nouveau, OncePerRequestFilter) : lit le header Authorization: Bearer <token>, extrait le login, charge le UserDetails via CustomUserDetailService, vérifie la validité du token, peuple le SecurityContextHolder. Un token absent, invalide, expiré ou pointant vers un utilisateur inconnu laisse la requête anonyme (rejetée en 401 par la chaîne de sécurité, pas d'exception 500).
  • SpringSecurityConfig : .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) — décommenté/branché.

Fichiers ajoutés (architecture en couches, DTO obligatoires)

Couche Fichier
Entité entities/Student.java (id, firstName, lastName, email, birthDate, createdAt, updatedAt)
DTO dto/StudentDTO.java (réponse), dto/StudentRequestDTO.java (création/modification, @NotBlank/@Email)
Mapper mapper/StudentDtoMapper.java (MapStruct, même pattern que UserDtoMapper)
Repository repository/StudentRepository.java (JpaRepository<Student, Long>)
Service service/StudentService.java (create/findAll/findById/update/delete)
Controller controller/StudentController.java (/api/students, aucune entité JPA exposée)
Erreurs handler/RestExceptionHandler.java — ajout d'un handler NoSuchElementException404

La table student est créée automatiquement au démarrage (spring.jpa.hibernate.ddl-auto: update), aucune migration manuelle nécessaire.

Endpoints

Méthode URL Auth requise Succès Erreurs
POST /api/students Oui 201 + StudentDTO 400 (validation), 401
GET /api/students Oui 200 + StudentDTO[] 401
GET /api/students/{id} Oui 200 + StudentDTO 404, 401
PUT /api/students/{id} Oui 200 + StudentDTO 400, 404, 401
DELETE /api/students/{id} Oui 204 404, 401

Comment reproduire (pour l'examinateur)

1. Démarrer l'environnement

# Docker Desktop doit tourner (Windows) — vérifier depuis WSL :
docker ps

cd back-end
mvn clean test          # 6/6 tests, ~20s (voir étape 2 pour le détail du fix Testcontainers)
mvn spring-boot:run      # démarre MySQL via docker-compose + l'API sur :8080

2. Obtenir un token JWT

curl -s -X POST http://localhost:8080/api/register \
  -H "Content-Type: application/json" \
  -d '{"firstName":"CRUD","lastName":"Tester","login":"crud.tester","password":"crud-pass"}'
# -> 201

TOKEN=$(curl -s -X POST http://localhost:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{"login":"crud.tester","password":"crud-pass"}')
echo "$TOKEN"

3. Vérifier la sécurité (sans token → 401)

curl -i http://localhost:8080/api/students
# -> HTTP/1.1 401

curl -i -X POST http://localhost:8080/api/students \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Jane","lastName":"Doe","email":"jane.doe@example.com","birthDate":"2000-05-10"}'
# -> HTTP/1.1 401

4. CRUD complet (avec token)

# CREATE
curl -i -X POST http://localhost:8080/api/students \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"firstName":"Jane","lastName":"Doe","email":"jane.doe@example.com","birthDate":"2000-05-10"}'
# -> 201 + { "id": 1, ... }

# READ (liste)
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/students
# -> 200 + [ { "id": 1, ... } ]

# READ (détail)
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/students/1
# -> 200 + { "id": 1, ... }

# READ (id inexistant)
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/students/999
# -> 404 + { "message": "Student with id 999 not found", ... }

# UPDATE
curl -i -X PUT http://localhost:8080/api/students/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"firstName":"Jane","lastName":"Doe-Updated","email":"jane.doe@example.com","birthDate":"2000-05-10"}'
# -> 200

# VALIDATION (corps vide)
curl -i -X POST http://localhost:8080/api/students \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{}'
# -> 400

# EMAIL DÉJÀ UTILISÉ (contrainte unique, vérifiée en amont par le service)
curl -i -X POST http://localhost:8080/api/students \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"firstName":"Dup","lastName":"Licate","email":"jane.doe@example.com","birthDate":"2000-05-10"}'
# -> 400 + { "message": "Student with email jane.doe@example.com already exists", ... }

# DELETE
curl -i -X DELETE http://localhost:8080/api/students/1 -H "Authorization: Bearer $TOKEN"
# -> 204

curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/students/1
# -> 404 (confirme la suppression)

# TOKEN INVALIDE (robustesse du filtre)
curl -i -H "Authorization: Bearer not-a-real-jwt" http://localhost:8080/api/students
# -> 401 (pas 500)

Vérifications effectuées dans cette session

Toutes les commandes ci-dessus ont été exécutées contre le back-end réel (MySQL via Docker Compose) :

Scénario Résultat obtenu
mvn clean test 6/6, BUILD SUCCESS
Table student créée au boot confirmé dans les logs Hibernate DDL
CRUD sans token 401 sur toutes les routes
CRUD avec token valide 201 / 200 / 200 / 204 conformes
GET id inexistant 404 avec message explicite
POST corps vide 400 (validation Bean Validation)
POST/PUT email déjà utilisé 400 avec message explicite (voir ci-dessous)
Token malformé 401 (pas d'erreur 500 côté filtre)

Détail : email en double détecté avant l'écriture en base

email est unique en base (contrainte SQL sur Student). Un premier essai laissait l'exception SQL (DataIntegrityViolationException) remonter jusqu'au handler générique Exception.class500 Internal Server error, peu explicite. Corrigé en suivant le même principe que UserService.register() (vérifier avant d'écrire plutôt que rattraper l'erreur SQL) :

// StudentService.create() / .update()
if (studentRepository.findByEmail(student.getEmail()).isPresent()) {
    throw new IllegalArgumentException("Student with email " + student.getEmail() + " already exists");
}

400 via le handler IllegalArgumentException déjà en place dans RestExceptionHandler, cohérent avec le reste de l'API.

Points de vigilance restants

  • Pas de tests JUnit dédiés à Student* pour l'instant — prévu à l'Exercice 2 (couverture ≥ 80 %), en suivant le même schéma que UserServiceTest/UserControllerTest.