É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 :
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 deextractUsername(),isTokenValid(),isTokenExpired()(parsing viaJwts.parser().verifyWith(...)).JwtAuthenticationFilter(nouveau,OncePerRequestFilter) : lit le headerAuthorization: Bearer <token>, extrait lelogin, charge leUserDetailsviaCustomUserDetailService, vérifie la validité du token, peuple leSecurityContextHolder. 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 NoSuchElementException → 404 |
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.class → 500 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 queUserServiceTest/UserControllerTest.