UserController.java
package com.archiweb.api;
import com.archiweb.dto.ParticipationDTO;
import com.archiweb.dto.RoleDTO;
import com.archiweb.dto.UserProfileDto;
import com.archiweb.dto.UserWithParticipationsDTO;
import com.archiweb.model.Participation;
import com.archiweb.model.Role;
import com.archiweb.model.User;
import com.archiweb.model.Win;
import com.archiweb.repository.RoleRepository;
import com.archiweb.repository.UserRepository;
import com.archiweb.repository.WinRepository;
import com.archiweb.security.JwtService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/users")
@Tag(
name = "Utilisateurs",
description = """
Gestion complète des utilisateurs :
- Consultation du profil et des rôles
- Mise à jour complète ou partielle du profil
- Suppression d’un compte utilisateur
- Consultation de l’historique des gains
- Pagination et filtrage des utilisateurs
"""
)
public class UserController {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final WinRepository winRepository;
private final JwtService jwtService;
public UserController(UserRepository userRepository,
RoleRepository roleRepository,
WinRepository winRepository,
JwtService jwtService) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.winRepository = winRepository;
this.jwtService = jwtService;
}
// ================================================================
// 1️⃣ Lister les utilisateurs (paginé)
// ================================================================
@Operation(
summary = "Lister les utilisateurs (paginé)",
description = """
Retourne la liste paginée des utilisateurs avec leurs rôles et informations de base.
Les paramètres `page` et `size` permettent de contrôler la pagination.
""",
parameters = {
@Parameter(name = "page", description = "Numéro de la page (par défaut : 0)"),
@Parameter(name = "size", description = "Taille de la page (par défaut : 10)")
},
responses = {
@ApiResponse(responseCode = "200", description = "Liste paginée des utilisateurs récupérée avec succès",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Page.class),
examples = @ExampleObject(value = """
{
"content": [
{
"id": 1,
"username": "john_doe",
"email": "john@example.com",
"role": { "name": "ROLE_USER" }
}
],
"pageable": { "pageNumber": 0, "pageSize": 10 },
"totalElements": 25
}
""")
))
}
)
@GetMapping
public ResponseEntity<Page<UserWithParticipationsDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
Pageable pageable = PageRequest.of(page, size);
// Charger les utilisateurs avec leurs participations (via @EntityGraph)
Page<User> users = userRepository.findAll(pageable);
Page<UserWithParticipationsDTO> dtoPage = users.map(user -> {
UserWithParticipationsDTO dto = new UserWithParticipationsDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setEmail(user.getEmail());
dto.setConsentGiven(user.isConsentGiven());
dto.setFirstName(user.getFirstName());
dto.setLastName(user.getLastName());
dto.setPhone(user.getPhone());
dto.setBirthDate(user.getBirthDate());
dto.setActive(user.isActive());
// Mapper le rôle en créant un DTO pour éviter les problèmes de proxy Hibernate
if (user.getRole() != null) {
// Forcer l'initialisation du rôle en accédant à ses propriétés
Role role = user.getRole();
RoleDTO roleDTO = new RoleDTO();
roleDTO.setId(role.getId());
roleDTO.setName(role.getName());
dto.setRole(roleDTO);
}
// Mapper les participations
if (user.getParticipations() != null && !user.getParticipations().isEmpty()) {
List<ParticipationDTO> participationDTOs = user.getParticipations().stream()
.map(participation -> {
ParticipationDTO pDto = new ParticipationDTO();
pDto.setId(participation.getId());
pDto.setCode(participation.getCode() != null ? participation.getCode().getValeur() : null);
pDto.setPrizeType(participation.getPrizeName() != null ? participation.getPrizeName() :
(participation.getCode() != null ? participation.getCode().getPrizeType().name() : null));
pDto.setClaimed(participation.isClaimed());
return pDto;
})
.collect(java.util.stream.Collectors.toList());
dto.setParticipations(participationDTOs);
} else {
dto.setParticipations(new java.util.ArrayList<>());
}
return dto;
});
return ResponseEntity.ok(dtoPage);
}
// ================================================================
// 2️⃣ Supprimer un utilisateur
// ================================================================
@Operation(
summary = "Supprimer un utilisateur",
description = "Supprime définitivement un utilisateur à partir de son identifiant unique.",
parameters = @Parameter(name = "id", description = "Identifiant de l'utilisateur à supprimer"),
responses = {
@ApiResponse(responseCode = "200", description = "Utilisateur supprimé avec succès",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
"Utilisateur supprimé avec succès"
"""))),
@ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
}
)
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
userRepository.deleteById(id);
return ResponseEntity.ok("Utilisateur supprimé avec succès");
}
// ================================================================
// 3️⃣ Mettre à jour complètement un utilisateur (PUT)
// ================================================================
@Operation(
summary = "Mettre à jour un utilisateur (PUT)",
description = """
Remplace toutes les informations d’un utilisateur existant par celles fournies dans la requête.
Les champs non fournis seront remplacés par des valeurs nulles.
""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Informations de l'utilisateur à mettre à jour",
required = true,
content = @Content(
schema = @Schema(implementation = User.class),
examples = @ExampleObject(value = """
{
"username": "john_doe",
"email": "john@example.com",
"role": { "name": "ROLE_ADMIN" }
}
""")
)
),
responses = {
@ApiResponse(responseCode = "200", description = "Utilisateur mis à jour avec succès"),
@ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
}
)
@PutMapping("/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
return userRepository.findById(id)
.map(user -> {
// Champs basiques
user.setUsername(updatedUser.getUsername());
user.setEmail(updatedUser.getEmail());
user.setFirstName(updatedUser.getFirstName());
user.setLastName(updatedUser.getLastName());
user.setPhone(updatedUser.getPhone());
user.setBirthDate(updatedUser.getBirthDate());
// Nouveau champ "active"
user.setActive(updatedUser.isActive());
// Consentement RGPD
user.setConsentGiven(updatedUser.isConsentGiven());
// Role
if (updatedUser.getRole() != null) {
Role role = roleRepository.findByName(updatedUser.getRole().getName())
.orElseThrow(() -> new RuntimeException("Rôle non trouvé : " + updatedUser.getRole().getName()));
user.setRole(role);
}
// Audit updateAt sera mis automatiquement via @PreUpdate
userRepository.save(user);
return ResponseEntity.ok(user);
})
.orElse(ResponseEntity.notFound().build());
}
// ================================================================
// 4️⃣ Récupérer le profil de l'utilisateur connecté
// ================================================================
@Operation(
summary = "Obtenir le profil de l'utilisateur connecté",
description = """
Récupère les informations de l'utilisateur à partir du token JWT transmis dans l'en-tête `Authorization`.
""",
parameters = @Parameter(name = "Authorization", description = "Header contenant le token JWT (ex: Bearer eyJhbGci...)"),
responses = {
@ApiResponse(responseCode = "200", description = "Utilisateur récupéré avec succès",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = User.class),
examples = @ExampleObject(value = """
{
"id": 1,
"email": "john@example.com",
"username": "john_doe",
"role": { "name": "ROLE_USER" }
}
""")
)),
@ApiResponse(responseCode = "400", description = "Token manquant ou invalide"),
@ApiResponse(responseCode = "404", description = "Utilisateur non trouvé")
}
)
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser(@RequestHeader("Authorization") String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.badRequest().body("Token manquant ou invalide");
}
String token = authHeader.substring(7);
String email = jwtService.extractEmail(token);
Optional<User> user = userRepository.findByEmail(email);
if (user.isEmpty()) {
return ResponseEntity.status(404).body("Utilisateur non trouvé");
}
User u = user.get();
return ResponseEntity.ok(u);
}
// ================================================================
// 5️⃣ Récupérer l'historique des gains d’un utilisateur
// ================================================================
@Operation(
summary = "Obtenir l’historique des gains d’un utilisateur",
description = """
Renvoie la liste complète des gains associés à un utilisateur à partir de son identifiant.
""",
parameters = @Parameter(name = "id", description = "Identifiant de l'utilisateur"),
responses = {
@ApiResponse(responseCode = "200", description = "Historique récupéré avec succès",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
[
{ "id": 1, "prizeType": "INFUSEUR", "date": "2025-01-15" },
{ "id": 2, "prizeType": "COFFRET_39", "date": "2025-02-10" }
]
""")
)),
@ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
}
)
@GetMapping("/{id}/wins")
public ResponseEntity<List<Win>> getUserWins(@PathVariable Long id) {
if (!userRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
List<Win> wins = winRepository.findByUserId(id);
return ResponseEntity.ok(wins);
}
// ================================================================
// 6️⃣ Mise à jour partielle (PATCH)
// ================================================================
@Operation(
summary = "Mettre à jour partiellement un utilisateur (PATCH)",
description = """
Permet de modifier uniquement certains champs du profil utilisateur sans écraser les autres données.
""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Champs à mettre à jour (un ou plusieurs)",
required = true,
content = @Content(
schema = @Schema(implementation = User.class),
examples = @ExampleObject(value = """
{
"firstName": "John",
"lastName": "Doe",
"phone": "+33612345678"
}
""")
)
),
responses = {
@ApiResponse(responseCode = "200", description = "Utilisateur mis à jour avec succès"),
@ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
}
)
@PatchMapping("/{id}")
public ResponseEntity<User> updatePartialUser(@PathVariable Long id, @RequestBody User updatedData) {
Optional<User> optionalUser = userRepository.findById(id);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
User user = optionalUser.get();
// Champs classiques
if (updatedData.getUsername() != null)
user.setUsername(updatedData.getUsername());
if (updatedData.getEmail() != null)
user.setEmail(updatedData.getEmail());
if (updatedData.getFirstName() != null)
user.setFirstName(updatedData.getFirstName());
if (updatedData.getLastName() != null)
user.setLastName(updatedData.getLastName());
if (updatedData.getPhone() != null)
user.setPhone(updatedData.getPhone());
if (updatedData.getBirthDate() != null)
user.setBirthDate(updatedData.getBirthDate());
// ✔ Nouveau champ : active
if (updatedData.isActive() != user.isActive())
user.setActive(updatedData.isActive());
// ✔ Nouveau champ : consentGiven
if (updatedData.isConsentGiven() != user.isConsentGiven())
user.setConsentGiven(updatedData.isConsentGiven());
// ✔ Préférences de cookies
if (updatedData.getCookiePreferences() != null)
user.setCookiePreferences(updatedData.getCookiePreferences());
if (updatedData.isCookieConsentGiven() != user.isCookieConsentGiven())
user.setCookieConsentGiven(updatedData.isCookieConsentGiven());
// ✔ Rôle si fourni - Vérification améliorée
if (updatedData.getRole() != null && updatedData.getRole().getName() != null) {
String roleName = updatedData.getRole().getName();
Optional<Role> roleOptional = roleRepository.findByName(roleName);
Role role;
if (roleOptional.isPresent()) {
role = roleOptional.get();
} else {
// Si le rôle n'existe pas, le créer
Role newRole = new Role();
newRole.setName(roleName);
newRole.setDescription("Rôle personnalisé : " + roleName);
newRole.setActive(true);
role = roleRepository.save(newRole);
}
// Toujours mettre à jour le rôle (même s'il est identique pour forcer la sauvegarde)
String currentRoleName = (user.getRole() != null) ? user.getRole().getName() : null;
if (!roleName.equals(currentRoleName)) {
user.setRole(role);
} else {
// Même si identique, on force la mise à jour pour s'assurer que la relation est bien sauvegardée
user.setRole(role);
}
}
User savedUser = userRepository.save(user);
return ResponseEntity.ok(user);
}
}