AuthController.java
package com.archiweb.api;
import com.archiweb.dto.LoginRequest;
import com.archiweb.dto.OAuthRequest;
import com.archiweb.dto.RefreshTokenRequest;
import com.archiweb.dto.RegisterRequest;
import com.archiweb.service.OAuthService;
import com.archiweb.model.Role;
import com.archiweb.model.User;
import com.archiweb.repository.RoleRepository;
import com.archiweb.repository.UserRepository;
import com.archiweb.security.JwtService;
import io.swagger.v3.oas.annotations.Operation;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
@Tag(
name = "Authentification",
description = """
Module complet de gestion de l'authentification des utilisateurs.
Ce contrôleur permet :
- L'inscription d'un nouvel utilisateur avec création automatique du rôle par défaut.
- La connexion et la génération d’un token JWT d’accès.
- Le rafraîchissement du token d’accès via un refresh token valide.
- La vérification de l’état opérationnel du module d’authentification (ping).
"""
)
public class AuthController {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final OAuthService oAuthService;
@Value("${admin.email:}")
private String adminEmail;
// =========================================================
// Inscription (REGISTER)
// =========================================================
@Operation(
summary = "Inscription d’un nouvel utilisateur",
description = """
Crée un nouveau compte utilisateur avec un mot de passe chiffré et le rôle par défaut **ROLE_USER**.
Retourne un token JWT d’accès immédiatement utilisable pour les appels sécurisés.
""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Données nécessaires à la création du compte",
required = true,
content = @Content(
schema = @Schema(implementation = RegisterRequest.class),
examples = @ExampleObject(
value = """
{
"email": "newuser@example.com",
"password": "StrongP@ssw0rd",
"username": "newuser"
}
"""
)
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Utilisateur créé avec succès",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"message": "Inscription réussie",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
"""
)
)
),
@ApiResponse(
responseCode = "400",
description = "Email déjà utilisé",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Email déjà utilisé\" }")
)
)
}
)
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
String email = request.getEmail();
String password = request.getPassword();
String username = request.getUsername() != null ? request.getUsername() : email.split("@")[0];
if (userRepository.findByEmail(email).isPresent()) {
return ResponseEntity.badRequest().body(Map.of("error", "Email déjà utilisé"));
}
Role userRole = roleRepository.findByName("ROLE_USER")
.orElseGet(() -> roleRepository.save(new Role(null, "ROLE_USER", "Rôle utilisateur standard. Permet de participer au jeu concours et de consulter son profil.", true)));
User user = new User();
user.setEmail(email);
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setRole(userRole);
userRepository.save(user);
String token = jwtService.generateToken(user);
return ResponseEntity.ok(Map.of(
"message", "Inscription réussie",
"accessToken", token
));
}
// =========================================================
// Connexion (LOGIN)
// =========================================================
@Operation(
summary = "Connexion",
description = """
Authentifie un utilisateur à partir de son **email** et **mot de passe**.
Retourne un token JWT d’accès si les identifiants sont valides.
""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Identifiants de connexion",
required = true,
content = @Content(
schema = @Schema(implementation = LoginRequest.class),
examples = @ExampleObject(
value = """
{
"email": "user@example.com",
"password": "StrongP@ssw0rd"
}
"""
)
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Connexion réussie",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"message": "Connexion réussie",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
"""
)
)
),
@ApiResponse(
responseCode = "401",
description = "Identifiants invalides",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Email ou mot de passe incorrect\" }")
)
)
}
)
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
String email = request.getEmail();
String password = request.getPassword();
Optional<User> optionalUser = userRepository.findByEmail(email);
if (optionalUser.isEmpty()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Email ou mot de passe incorrect"));
}
User user = optionalUser.get();
if (!passwordEncoder.matches(password, user.getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Email ou mot de passe incorrect"));
}
// ✅ FORCER l'activation TOUJOURS pour contourner le problème de mapping bit(1) -> boolean
// Même si isActive() retourne true, on force l'activation pour s'assurer que la valeur est bien persistée
// Cela résout le problème où Hibernate lit false même avec active=1 en base
user.setActive(true);
userRepository.save(user);
String token = jwtService.generateToken(user);
return ResponseEntity.ok(Map.of(
"message", "Connexion réussie",
"accessToken", token
));
}
// =========================================================
// Rafraîchir le token (REFRESH)
// =========================================================
@Operation(
summary = "Rafraîchir le token JWT",
description = """
Permet de générer un **nouveau token d’accès** à partir d’un refresh token valide.
Si le refresh token est invalide ou expiré, une erreur 401 est retournée.
""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Refresh token à valider",
required = true,
content = @Content(
schema = @Schema(implementation = RefreshTokenRequest.class),
examples = @ExampleObject(
value = """
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
"""
)
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Nouveau token généré avec succès",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"message": "Nouveau token généré avec succès",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
"""
)
)
),
@ApiResponse(
responseCode = "400",
description = "Refresh token manquant",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Missing refresh token\" }")
)
),
@ApiResponse(
responseCode = "401",
description = "Refresh token invalide ou expiré",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Invalid refresh token\" }")
)
)
}
)
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
String oldToken = request.getRefreshToken();
if (oldToken == null || oldToken.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Missing refresh token"));
}
try {
String newToken = jwtService.refreshToken(oldToken);
return ResponseEntity.ok(Map.of(
"message", "Nouveau token généré avec succès",
"accessToken", newToken
));
} catch (Exception e) {
return ResponseEntity.status(401).body(Map.of("error", "Invalid refresh token"));
}
}
// =========================================================
// Vérification du module (PING)
// =========================================================
@Operation(
summary = "Vérification du module d'authentification",
description = """
Vérifie que le module d’authentification est bien opérationnel et accessible.
Utile pour les tests d’intégration et la supervision.
""",
responses = @ApiResponse(
responseCode = "200",
description = "Module opérationnel",
content = @Content(
mediaType = "text/plain",
examples = @ExampleObject(value = "AuthController opérationnel")
)
)
)
@GetMapping("/ping")
public ResponseEntity<String> ping() {
return ResponseEntity.ok("AuthController opérationnel");
}
// =========================================================
// OAuth Login (Google / Facebook)
// =========================================================
@Operation(
summary = "Connexion via OAuth (Google/Facebook)",
description = """
Authentifie un utilisateur via un token OAuth (Google ou Facebook).
Si l'utilisateur n'existe pas, il sera créé automatiquement.
""",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Token OAuth et informations utilisateur",
required = true,
content = @Content(
schema = @Schema(implementation = OAuthRequest.class),
examples = @ExampleObject(
value = """
{
"token": "ya29.a0AfH6SMC...",
"provider": "google",
"email": "user@gmail.com",
"firstName": "John",
"lastName": "Doe"
}
"""
)
)
),
responses = {
@ApiResponse(
responseCode = "200",
description = "Connexion réussie",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(
value = """
{
"message": "Connexion OAuth réussie",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"isNewUser": false
}
"""
)
)
),
@ApiResponse(
responseCode = "401",
description = "Token OAuth invalide",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = "{ \"error\": \"Token OAuth invalide\" }")
)
)
}
)
@PostMapping("/oauth")
public ResponseEntity<?> oauthLogin(@RequestBody OAuthRequest request) {
String token = request.getToken();
String provider = request.getProvider();
String email = request.getEmail();
String firstName = request.getFirstName();
String lastName = request.getLastName();
String username = request.getUsername();
if (token == null || token.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Token OAuth manquant"));
}
// Vérifier le token selon le provider
Map<String, String> userInfo;
if ("google".equalsIgnoreCase(provider)) {
userInfo = oAuthService.verifyGoogleToken(token);
} else if ("facebook".equalsIgnoreCase(provider)) {
userInfo = oAuthService.verifyFacebookToken(token);
} else {
return ResponseEntity.badRequest().body(Map.of("error", "Provider non supporté. Utilisez 'google' ou 'facebook'"));
}
// Si la vérification du token échoue mais que le frontend a fourni des informations utilisateur,
// on accepte quand même la connexion (le frontend a déjà vérifié le token)
// Cela permet de gérer les cas où le token expire entre le moment où le frontend le vérifie
// et le moment où le backend le vérifie, ou les problèmes de réseau temporaires
boolean tokenValid = "true".equals(userInfo.get("valid"));
// Vérifier que nous avons au moins un email (le plus important)
// Les autres champs peuvent avoir des valeurs par défaut
boolean hasEmailFromRequest = email != null && !email.isBlank();
String emailFromProvider = userInfo.get("email");
boolean hasEmailFromProvider = emailFromProvider != null && !emailFromProvider.isBlank();
// Log pour déboguer
log.info("OAuth Login - Provider: {}, TokenValid: {}, HasEmailFromRequest: {}, HasEmailFromProvider: {}, Email: {}",
provider, tokenValid, hasEmailFromRequest, hasEmailFromProvider,
email != null ? email.replaceAll("@.*", "@***") : "null");
// Si on n'a pas d'email du tout (ni de la requête ni du provider), rejeter
if (!hasEmailFromRequest && !hasEmailFromProvider) {
log.warn("OAuth Login rejeté - Aucun email disponible (ni requête ni provider)");
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "Email non disponible. Veuillez réessayer."));
}
// Si le token n'est pas valide MAIS qu'on a un email valide dans la requête, on accepte quand même
// Le frontend a déjà vérifié le token, donc on fait confiance aux informations fournies
if (!tokenValid && !hasEmailFromRequest) {
log.warn("OAuth Login rejeté - Token invalide et pas d'email dans la requête");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Token OAuth invalide ou expiré. Veuillez réessayer."));
}
// Si le token est invalide mais qu'on a un email valide, on continue (le frontend a vérifié)
if (!tokenValid && hasEmailFromRequest) {
log.info("OAuth Login - Token invalide côté backend mais email valide fourni par le frontend, on accepte");
}
// Utiliser les infos du token si disponibles, sinon celles de la requête
// Pour l'email : si le provider ne le fournit pas, utiliser celui de la requête
// L'email de la requête a la priorité car le frontend l'a déjà vérifié
if (hasEmailFromProvider && !hasEmailFromRequest) {
// Si on n'a que l'email du provider, l'utiliser
email = emailFromProvider;
} else if (hasEmailFromRequest) {
// L'email de la requête a la priorité (déjà vérifié par le frontend)
// email reste tel quel
}
// Sinon, email est déjà défini et valide
// Utiliser les infos du provider si disponibles, sinon celles de la requête
// Avec des valeurs par défaut si tout est vide
String firstNameFromProvider = userInfo.get("firstName");
String lastNameFromProvider = userInfo.get("lastName");
if (firstNameFromProvider != null && !firstNameFromProvider.isBlank()) {
firstName = firstNameFromProvider;
} else if (firstName == null || firstName.isBlank()) {
// Valeur par défaut si aucune info n'est disponible
firstName = "Utilisateur";
}
if (lastNameFromProvider != null && !lastNameFromProvider.isBlank()) {
lastName = lastNameFromProvider;
} else if (lastName == null || lastName.isBlank()) {
// Valeur par défaut selon le provider
lastName = "google".equalsIgnoreCase(provider) ? "Google" : "Facebook";
}
// Pour le username, utiliser celui du provider ou de la requête, ou générer depuis l'email
String usernameFromProvider = userInfo.get("username");
if (usernameFromProvider != null && !usernameFromProvider.isBlank()) {
username = usernameFromProvider;
} else if (username == null || username.isBlank()) {
// email est garanti d'être non-null à ce point (vérifié plus haut)
if (email != null) {
username = email.split("@")[0];
} else {
username = "user";
}
}
// Chercher ou créer l'utilisateur
Optional<User> optionalUser = userRepository.findByEmail(email);
User user;
boolean isNewUser = false;
if (optionalUser.isPresent()) {
user = optionalUser.get();
// ✅ FORCER l'activation pour OAuth (même si isActive() retourne false à cause du mapping bit(1))
// Si l'utilisateur se connecte via OAuth, c'est qu'il veut utiliser son compte
user.setActive(true); // Toujours activer, pas de condition
// Vérifier si l'email correspond à l'admin et promouvoir en ADMIN si nécessaire
if (adminEmail != null && !adminEmail.isBlank() && email.equalsIgnoreCase(adminEmail)) {
Role adminRole = roleRepository.findByName("ROLE_ADMIN")
.orElseGet(() -> roleRepository.save(new Role(null, "ROLE_ADMIN", "Rôle administrateur. Accès complet à toutes les fonctionnalités d'administration du système.", true)));
if (user.getRole() == null || !"ROLE_ADMIN".equals(user.getRole().getName())) {
user.setRole(adminRole);
log.info("OAuth Login - Promotion en ADMIN pour l'utilisateur existant: {}", email.replaceAll("@.*", "@***"));
}
}
// Mettre à jour les infos si disponibles
if (firstName != null && !firstName.isBlank()) user.setFirstName(firstName);
if (lastName != null && !lastName.isBlank()) user.setLastName(lastName);
userRepository.save(user);
} else {
// Créer un nouvel utilisateur
// Vérifier si l'email correspond à l'admin configuré
Role assignedRole;
if (adminEmail != null && !adminEmail.isBlank() && email.equalsIgnoreCase(adminEmail)) {
// Si l'email correspond à l'admin, lui donner le rôle ADMIN
assignedRole = roleRepository.findByName("ROLE_ADMIN")
.orElseGet(() -> roleRepository.save(new Role(null, "ROLE_ADMIN", "Rôle administrateur. Accès complet à toutes les fonctionnalités d'administration du système.", true)));
log.info("OAuth Login - Email admin détecté, attribution du rôle ADMIN à: {}", email.replaceAll("@.*", "@***"));
} else {
// Sinon, rôle USER par défaut
assignedRole = roleRepository.findByName("ROLE_USER")
.orElseGet(() -> roleRepository.save(new Role(null, "ROLE_USER", "Rôle utilisateur standard. Permet de participer au jeu concours et de consulter son profil.", true)));
}
user = new User();
user.setEmail(email);
user.setUsername(username);
user.setFirstName(firstName);
user.setLastName(lastName);
user.setRole(assignedRole);
// Pas de mot de passe pour les utilisateurs OAuth
user.setPassword(null);
user.setActive(true); // ✅ Activer les nouveaux utilisateurs OAuth
user.setConsentGiven(true); // ✅ Consentement donné par défaut pour OAuth
userRepository.save(user);
isNewUser = true;
}
String jwtToken = jwtService.generateToken(user);
return ResponseEntity.ok(Map.of(
"message", "Connexion OAuth réussie",
"accessToken", jwtToken,
"isNewUser", isNewUser
));
}
}