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:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user