package dev.lions.btpxpress.adapter.http; import jakarta.annotation.security.PermitAll; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.Map; import org.eclipse.microprofile.jwt.JsonWebToken; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Resource REST pour l'authentification et les informations utilisateur * Permet de récupérer les informations de l'utilisateur connecté depuis le token JWT Keycloak */ @Path("/api/v1/auth") @Produces(MediaType.APPLICATION_JSON) @Tag(name = "Authentification", description = "Gestion de l'authentification et informations utilisateur") public class AuthResource { private static final Logger logger = LoggerFactory.getLogger(AuthResource.class); @Inject JsonWebToken jwt; /** * Redirige vers Keycloak pour l'authentification * Architecture 2025 : Redirection directe vers https://security.lions.dev pour l'authentification */ @GET @Path("/login") @PermitAll @Operation( summary = "Initier l'authentification Keycloak", description = "Redirige l'utilisateur vers Keycloak (https://security.lions.dev) pour l'authentification OAuth2/OIDC") @APIResponse(responseCode = "302", description = "Redirection vers Keycloak pour authentification") public Response login(@Context SecurityContext securityContext) { try { logger.info("Redirection vers Keycloak pour authentification"); // Construction de l'URL Keycloak pour l'authentification String keycloakUrl = "https://security.lions.dev/realms/btpxpress/protocol/openid_connect/auth"; String clientId = "btpxpress-backend"; String redirectUri = "http://localhost:8080/api/v1/auth/callback"; // Peut être configuré dynamiquement String responseType = "code"; String scope = "openid profile email"; // Construction de l'URL complète avec paramètres java.net.URI authUri = java.net.URI.create( String.format( "%s?client_id=%s&redirect_uri=%s&response_type=%s&scope=%s", keycloakUrl, clientId, URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), responseType, URLEncoder.encode(scope, StandardCharsets.UTF_8) ) ); logger.debug("Redirection vers Keycloak: {}", authUri); return Response.status(Response.Status.FOUND) .location(authUri) .build(); } catch (Exception e) { logger.error("Erreur lors de la redirection vers Keycloak", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Erreur lors de la redirection vers Keycloak", "message", e.getMessage())) .build(); } } /** * Récupère les informations de l'utilisateur connecté depuis le token JWT */ @GET @Path("/user") @PermitAll // Accessible même sans authentification pour les tests @Operation( summary = "Informations utilisateur connecté", description = "Récupère les informations de l'utilisateur connecté depuis le token JWT Keycloak") @APIResponse(responseCode = "200", description = "Informations utilisateur récupérées") @APIResponse(responseCode = "401", description = "Non authentifié") public Response getCurrentUser(@Context SecurityContext securityContext) { try { logger.debug("Récupération des informations utilisateur connecté"); // Vérifier si l'utilisateur est authentifié Principal principal = securityContext.getUserPrincipal(); if (principal == null || jwt == null) { logger.warn("Aucun utilisateur authentifié trouvé"); // En mode développement, retourner un utilisateur de test return Response.ok(createTestUser()).build(); } // Extraire les informations du token JWT String userId = jwt.getSubject(); String username = jwt.getClaim("preferred_username"); String email = jwt.getClaim("email"); String firstName = jwt.getClaim("given_name"); String lastName = jwt.getClaim("family_name"); String fullName = jwt.getClaim("name"); // Extraire les rôles Object realmAccess = jwt.getClaim("realm_access"); Object resourceAccess = jwt.getClaim("resource_access"); // Construire la réponse avec les informations utilisateur Map userInfo = new java.util.HashMap<>(); userInfo.put("id", userId != null ? userId : "unknown"); userInfo.put("username", username != null ? username : email); userInfo.put("email", email != null ? email : "unknown@btpxpress.com"); userInfo.put("firstName", firstName != null ? firstName : "Utilisateur"); userInfo.put("lastName", lastName != null ? lastName : "Connecté"); userInfo.put("fullName", fullName != null ? fullName : (firstName + " " + lastName).trim()); userInfo.put("roles", extractRoles(realmAccess, resourceAccess)); userInfo.put("permissions", extractPermissions(realmAccess, resourceAccess)); userInfo.put("isAdmin", isAdmin(realmAccess, resourceAccess)); userInfo.put("isManager", isManager(realmAccess, resourceAccess)); userInfo.put("isEmployee", isEmployee(realmAccess, resourceAccess)); userInfo.put("isClient", isClient(realmAccess, resourceAccess)); logger.info("Informations utilisateur récupérées: {} ({})", username, email); return Response.ok(userInfo).build(); } catch (Exception e) { logger.error("Erreur lors de la récupération des informations utilisateur", e); // En cas d'erreur, retourner un utilisateur de test en mode développement return Response.ok(createTestUser()).build(); } } /** * Endpoint de test pour vérifier l'état de l'authentification */ @GET @Path("/status") @PermitAll @Operation( summary = "Statut d'authentification", description = "Vérifie l'état de l'authentification de l'utilisateur") @APIResponse(responseCode = "200", description = "Statut récupéré") public Response getAuthStatus(@Context SecurityContext securityContext) { try { Principal principal = securityContext.getUserPrincipal(); boolean isAuthenticated = principal != null && jwt != null; Map status = Map.of( "authenticated", isAuthenticated, "principal", principal != null ? principal.getName() : null, "hasJWT", jwt != null, "timestamp", System.currentTimeMillis() ); return Response.ok(status).build(); } catch (Exception e) { logger.error("Erreur lors de la vérification du statut d'authentification", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(Map.of("error", "Erreur lors de la vérification du statut")) .build(); } } /** * Crée un utilisateur de test pour le mode développement */ private Map createTestUser() { Map testUser = new java.util.HashMap<>(); testUser.put("id", "dev-user-001"); testUser.put("username", "admin.btpxpress"); testUser.put("email", "admin@btpxpress.com"); testUser.put("firstName", "Jean-Michel"); testUser.put("lastName", "Martineau"); testUser.put("fullName", "Jean-Michel Martineau"); testUser.put("roles", java.util.List.of("SUPER_ADMIN", "ADMIN", "DIRECTEUR")); testUser.put("permissions", java.util.List.of("ALL_PERMISSIONS")); testUser.put("isAdmin", true); testUser.put("isManager", true); testUser.put("isEmployee", false); testUser.put("isClient", false); return testUser; } /** * Extrait les rôles depuis les claims JWT */ private java.util.List extractRoles(Object realmAccess, Object resourceAccess) { java.util.List roles = new java.util.ArrayList<>(); // Ajouter les rôles du realm if (realmAccess instanceof Map) { Object realmRoles = ((Map) realmAccess).get("roles"); if (realmRoles instanceof java.util.List) { ((java.util.List) realmRoles).forEach(role -> { if (role instanceof String) { roles.add((String) role); } }); } } // Ajouter les rôles des ressources if (resourceAccess instanceof Map) { ((Map) resourceAccess).values().forEach(resource -> { if (resource instanceof Map) { Object resourceRoles = ((Map) resource).get("roles"); if (resourceRoles instanceof java.util.List) { ((java.util.List) resourceRoles).forEach(role -> { if (role instanceof String) { roles.add((String) role); } }); } } }); } return roles.isEmpty() ? java.util.List.of("USER") : roles; } /** * Extrait les permissions depuis les rôles */ private java.util.List extractPermissions(Object realmAccess, Object resourceAccess) { java.util.List roles = extractRoles(realmAccess, resourceAccess); java.util.List permissions = new java.util.ArrayList<>(); // Mapper les rôles vers les permissions for (String role : roles) { switch (role.toUpperCase()) { case "SUPER_ADMIN": case "BTPXPRESS_SUPER_ADMIN": permissions.add("ALL_PERMISSIONS"); break; case "ADMIN": case "BTPXPRESS_ADMIN": permissions.addAll(java.util.List.of("MANAGE_USERS", "MANAGE_CHANTIERS", "MANAGE_CLIENTS")); break; case "DIRECTEUR": case "MANAGER": permissions.addAll(java.util.List.of("VIEW_REPORTS", "MANAGE_CHANTIERS")); break; case "CHEF_CHANTIER": permissions.addAll(java.util.List.of("MANAGE_CHANTIER", "VIEW_PLANNING")); break; default: permissions.add("VIEW_BASIC"); break; } } return permissions.isEmpty() ? java.util.List.of("VIEW_BASIC") : permissions; } /** * Vérifie si l'utilisateur est administrateur */ private boolean isAdmin(Object realmAccess, Object resourceAccess) { java.util.List roles = extractRoles(realmAccess, resourceAccess); return roles.stream().anyMatch(role -> role.toUpperCase().contains("ADMIN") || role.toUpperCase().contains("SUPER_ADMIN")); } /** * Vérifie si l'utilisateur est manager */ private boolean isManager(Object realmAccess, Object resourceAccess) { java.util.List roles = extractRoles(realmAccess, resourceAccess); return roles.stream().anyMatch(role -> role.toUpperCase().contains("DIRECTEUR") || role.toUpperCase().contains("MANAGER") || role.toUpperCase().contains("CHEF")); } /** * Vérifie si l'utilisateur est employé */ private boolean isEmployee(Object realmAccess, Object resourceAccess) { java.util.List roles = extractRoles(realmAccess, resourceAccess); return roles.stream().anyMatch(role -> role.toUpperCase().contains("EMPLOYE") || role.toUpperCase().contains("OUVRIER")); } /** * Vérifie si l'utilisateur est client */ private boolean isClient(Object realmAccess, Object resourceAccess) { java.util.List roles = extractRoles(realmAccess, resourceAccess); return roles.stream().anyMatch(role -> role.toUpperCase().contains("CLIENT")); } }