Refactor: Backend Frontend-Centric Auth - Suppression OIDC, validation JWT

Architecture modifiée pour Frontend-Centric Authentication:

1. **Suppression des dépendances OIDC**
   - quarkus-oidc → quarkus-smallrye-jwt
   - quarkus-keycloak-authorization → quarkus-smallrye-jwt-build
   - Le backend ne gère plus l'authentification OAuth

2. **Configuration JWT simple**
   - Validation des tokens JWT envoyés par le frontend
   - mp.jwt.verify.publickey.location (JWKS de Keycloak)
   - mp.jwt.verify.issuer (Keycloak realm)
   - Authentification via Authorization: Bearer header

3. **Suppression configurations OIDC**
   - application.properties: Suppression %dev.quarkus.oidc.*
   - application.properties: Suppression %prod.quarkus.oidc.*
   - application-prod.properties: Remplacement par mp.jwt.*
   - Logging: io.quarkus.oidc → io.quarkus.smallrye.jwt

4. **Sécurité simplifiée**
   - quarkus.security.auth.proactive=false
   - @Authenticated sur les endpoints
   - CORS configuré pour le frontend
   - Endpoints publics: /q/*, /openapi, /swagger-ui/*

Flux d'authentification:
1️⃣ Frontend → Keycloak (OAuth login)
2️⃣ Frontend ← Keycloak (access_token)
3️⃣ Frontend → Backend (Authorization: Bearer token)
4️⃣ Backend valide le token JWT (signature + issuer)
5️⃣ Backend → Frontend (données API)

Avantages:
 Pas de secret backend à gérer
 Pas de client btpxpress-backend dans Keycloak
 Séparation claire frontend/backend
 Backend devient une API REST stateless
 Tokens gérés par le frontend (localStorage/sessionStorage)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DahoudG
2025-10-31 17:05:11 +00:00
parent 7a72d13ffa
commit 7df5f346f1
60 changed files with 6095 additions and 4932 deletions

View File

@@ -0,0 +1,359 @@
package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.AbonnementService;
import dev.lions.btpxpress.domain.core.entity.Abonnement;
import dev.lions.btpxpress.domain.core.entity.StatutAbonnement;
import dev.lions.btpxpress.domain.core.entity.TypeAbonnement;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
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 la gestion des abonnements
* Architecture 2025 : API complète pour la gestion des abonnements
*/
@Path("/api/v1/abonnements")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Abonnements", description = "Gestion des abonnements d'entreprise")
public class AbonnementResource {
private static final Logger logger = LoggerFactory.getLogger(AbonnementResource.class);
@Inject AbonnementService abonnementService;
// === ENDPOINTS DE LECTURE ===
@GET
@Operation(summary = "Récupérer tous les abonnements")
@APIResponse(responseCode = "200", description = "Liste des abonnements récupérée avec succès")
public Response getAllAbonnements(
@Parameter(description = "Statut") @QueryParam("statut") String statut,
@Parameter(description = "Type d'abonnement") @QueryParam("type") String type) {
logger.debug("GET /abonnements - statut: {}, type: {}", statut, type);
List<Abonnement> abonnements;
if (statut != null && !statut.trim().isEmpty()) {
try {
StatutAbonnement statutEnum = StatutAbonnement.valueOf(statut.toUpperCase());
abonnements = abonnementService.findByStatut(statutEnum);
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Statut invalide: " + statut))
.build();
}
} else if (type != null && !type.trim().isEmpty()) {
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
abonnements = abonnementService.findByType(typeEnum);
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type invalide: " + type))
.build();
}
} else {
abonnements = abonnementService.findAll();
}
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/{id}")
@Operation(summary = "Récupérer un abonnement par ID")
@APIResponse(responseCode = "200", description = "Abonnement trouvé")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response getAbonnementById(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.debug("GET /abonnements/{}", id);
Abonnement abonnement = abonnementService.findByIdRequired(id);
return Response.ok(abonnement).build();
}
@GET
@Path("/actifs")
@Operation(summary = "Récupérer tous les abonnements actifs")
@APIResponse(responseCode = "200", description = "Liste des abonnements actifs")
public Response getAbonnementsActifs() {
logger.debug("GET /abonnements/actifs");
List<Abonnement> abonnements = abonnementService.findActifs();
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/expires")
@Operation(summary = "Récupérer tous les abonnements expirés")
@APIResponse(responseCode = "200", description = "Liste des abonnements expirés")
public Response getAbonnementsExpires() {
logger.debug("GET /abonnements/expires");
List<Abonnement> abonnements = abonnementService.findExpires();
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/bientot-expires")
@Operation(summary = "Récupérer les abonnements qui arrivent à expiration")
@APIResponse(responseCode = "200", description = "Liste des abonnements bientôt expirés")
public Response getAbonnementsBientotExpires(
@Parameter(description = "Nombre de jours") @QueryParam("jours") @DefaultValue("7") int jours) {
logger.debug("GET /abonnements/bientot-expires - jours: {}", jours);
List<Abonnement> abonnements = abonnementService.findBientotExpires(jours);
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/entreprise/{entrepriseId}")
@Operation(summary = "Récupérer l'abonnement actif d'une entreprise")
@APIResponse(responseCode = "200", description = "Abonnement actif trouvé")
@APIResponse(responseCode = "404", description = "Aucun abonnement actif pour cette entreprise")
public Response getAbonnementActifByEntreprise(
@Parameter(description = "ID de l'entreprise") @PathParam("entrepriseId") UUID entrepriseId) {
logger.debug("GET /abonnements/entreprise/{}", entrepriseId);
return abonnementService
.findAbonnementActifByEntreprise(entrepriseId)
.map(abonnement -> Response.ok(abonnement).build())
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Aucun abonnement actif pour cette entreprise"))
.build());
}
@GET
@Path("/entreprise/{entrepriseId}/historique")
@Operation(summary = "Récupérer l'historique des abonnements d'une entreprise")
@APIResponse(responseCode = "200", description = "Historique récupéré avec succès")
public Response getHistoriqueByEntreprise(
@Parameter(description = "ID de l'entreprise") @PathParam("entrepriseId") UUID entrepriseId) {
logger.debug("GET /abonnements/entreprise/{}/historique", entrepriseId);
List<Abonnement> abonnements = abonnementService.findByEntreprise(entrepriseId);
Map<String, Object> response = new HashMap<>();
response.put("abonnements", abonnements);
response.put("total", abonnements.size());
return Response.ok(response).build();
}
@GET
@Path("/statistics")
@Operation(summary = "Récupérer les statistiques des abonnements")
@APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès")
public Response getStatistics() {
logger.debug("GET /abonnements/statistics");
Map<String, Object> stats = abonnementService.getStatistics();
return Response.ok(stats).build();
}
@GET
@Path("/plans")
@Operation(summary = "Récupérer les plans tarifaires disponibles")
@APIResponse(responseCode = "200", description = "Plans récupérés avec succès")
public Response getPlans() {
logger.debug("GET /abonnements/plans");
Map<String, Object> plans = abonnementService.getPlans();
return Response.ok(plans).build();
}
// === ENDPOINTS DE CRÉATION ===
@POST
@Authenticated
@Operation(summary = "Créer un nouvel abonnement")
@APIResponse(responseCode = "201", description = "Abonnement créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
@APIResponse(responseCode = "409", description = "Un abonnement actif existe déjà")
public Response createAbonnement(@Valid @NotNull Abonnement abonnement) {
logger.info("POST /abonnements - Création d'un abonnement");
Abonnement created = abonnementService.create(abonnement);
return Response.status(Response.Status.CREATED).entity(created).build();
}
@POST
@Path("/mensuel")
@Authenticated
@Operation(summary = "Créer un abonnement mensuel")
@APIResponse(responseCode = "201", description = "Abonnement mensuel créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createAbonnementMensuel(
@Parameter(description = "ID de l'entreprise") @QueryParam("entrepriseId") @NotNull UUID entrepriseId,
@Parameter(description = "Type d'abonnement") @QueryParam("type") @NotNull String type,
@Parameter(description = "Méthode de paiement") @QueryParam("methodePaiement") String methodePaiement) {
logger.info(
"POST /abonnements/mensuel - entrepriseId: {}, type: {}", entrepriseId, type);
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
Abonnement created =
abonnementService.createAbonnementMensuel(entrepriseId, typeEnum, methodePaiement);
return Response.status(Response.Status.CREATED).entity(created).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type d'abonnement invalide: " + type))
.build();
}
}
@POST
@Path("/annuel")
@Authenticated
@Operation(summary = "Créer un abonnement annuel")
@APIResponse(responseCode = "201", description = "Abonnement annuel créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides")
public Response createAbonnementAnnuel(
@Parameter(description = "ID de l'entreprise") @QueryParam("entrepriseId") @NotNull UUID entrepriseId,
@Parameter(description = "Type d'abonnement") @QueryParam("type") @NotNull String type,
@Parameter(description = "Méthode de paiement") @QueryParam("methodePaiement") String methodePaiement) {
logger.info("POST /abonnements/annuel - entrepriseId: {}, type: {}", entrepriseId, type);
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
Abonnement created =
abonnementService.createAbonnementAnnuel(entrepriseId, typeEnum, methodePaiement);
return Response.status(Response.Status.CREATED).entity(created).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type d'abonnement invalide: " + type))
.build();
}
}
// === ENDPOINTS DE MISE À JOUR ===
@PUT
@Path("/{id}")
@Authenticated
@Operation(summary = "Mettre à jour un abonnement")
@APIResponse(responseCode = "200", description = "Abonnement mis à jour avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response updateAbonnement(
@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id,
@Valid @NotNull Abonnement abonnementUpdate) {
logger.info("PUT /abonnements/{}", id);
Abonnement updated = abonnementService.update(id, abonnementUpdate);
return Response.ok(updated).build();
}
@POST
@Path("/{id}/renouveler")
@Authenticated
@Operation(summary = "Renouveler un abonnement")
@APIResponse(responseCode = "200", description = "Abonnement renouvelé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response renouvelerAbonnement(
@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id,
@Parameter(description = "Renouvellement annuel") @QueryParam("annuel") @DefaultValue("false") boolean annuel) {
logger.info("POST /abonnements/{}/renouveler - annuel: {}", id, annuel);
Abonnement renewed = abonnementService.renouveler(id, annuel);
return Response.ok(renewed).build();
}
@PUT
@Path("/{id}/changer-type")
@Authenticated
@Operation(summary = "Changer le type d'abonnement (upgrade/downgrade)")
@APIResponse(responseCode = "200", description = "Type changé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
@APIResponse(responseCode = "400", description = "Type invalide")
public Response changerType(
@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id,
@Parameter(description = "Nouveau type") @QueryParam("type") @NotNull String type) {
logger.info("PUT /abonnements/{}/changer-type - nouveauType: {}", id, type);
try {
TypeAbonnement typeEnum = TypeAbonnement.valueOf(type.toUpperCase());
Abonnement updated = abonnementService.changerType(id, typeEnum);
return Response.ok(updated).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Type d'abonnement invalide: " + type))
.build();
}
}
@PUT
@Path("/{id}/toggle-auto-renew")
@Authenticated
@Operation(summary = "Activer/désactiver le renouvellement automatique")
@APIResponse(responseCode = "200", description = "Renouvellement automatique modifié")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response toggleAutoRenew(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("PUT /abonnements/{}/toggle-auto-renew", id);
Abonnement updated = abonnementService.toggleAutoRenouvellement(id);
return Response.ok(updated).build();
}
@POST
@Path("/{id}/annuler")
@Authenticated
@Operation(summary = "Annuler un abonnement")
@APIResponse(responseCode = "204", description = "Abonnement annulé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response annulerAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("POST /abonnements/{}/annuler", id);
abonnementService.annuler(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
@POST
@Path("/{id}/suspendre")
@Authenticated
@Operation(summary = "Suspendre un abonnement")
@APIResponse(responseCode = "204", description = "Abonnement suspendu avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response suspendreAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("POST /abonnements/{}/suspendre", id);
abonnementService.suspendre(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
@POST
@Path("/{id}/reactiver")
@Authenticated
@Operation(summary = "Réactiver un abonnement suspendu")
@APIResponse(responseCode = "204", description = "Abonnement réactivé avec succès")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response reactiverAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("POST /abonnements/{}/reactiver", id);
abonnementService.reactiver(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
// === ENDPOINTS DE SUPPRESSION ===
@DELETE
@Path("/{id}")
@Authenticated
@Operation(summary = "Supprimer définitivement un abonnement")
@APIResponse(responseCode = "204", description = "Abonnement supprimé définitivement")
@APIResponse(responseCode = "404", description = "Abonnement non trouvé")
public Response deleteAbonnement(@Parameter(description = "ID de l'abonnement") @PathParam("id") UUID id) {
logger.info("DELETE /abonnements/{}", id);
abonnementService.deletePermanently(id);
return Response.status(Response.Status.NO_CONTENT).build();
}
}