Refactoring
This commit is contained in:
128
.env.example
Normal file
128
.env.example
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# ============================================
|
||||||
|
# AfterWork API - Configuration
|
||||||
|
# ============================================
|
||||||
|
# Copiez ce fichier vers .env et remplissez les valeurs appropriées
|
||||||
|
# NE JAMAIS COMMITTER le fichier .env !
|
||||||
|
#
|
||||||
|
# ==== INFRASTRUCTURE LIONS (Production) ====
|
||||||
|
# - API Gateway: https://api.lions.dev/afterwork
|
||||||
|
# - PostgreSQL: postgresql-service.postgresql.svc.cluster.local:5432
|
||||||
|
# - Kafka: kafka-service.kafka.svc.cluster.local:9092
|
||||||
|
# - Prometheus: https://prometheus.lions.dev
|
||||||
|
# - Grafana: https://grafana.lions.dev
|
||||||
|
# - Vault: https://vault.lions.dev
|
||||||
|
# - Keycloak: https://security.lions.dev
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# BASE DE DONNÉES
|
||||||
|
# ============================================
|
||||||
|
# === Développement local ===
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=afterwork_dev
|
||||||
|
DB_USERNAME=skyfile
|
||||||
|
DB_PASSWORD=skyfile
|
||||||
|
|
||||||
|
# === Production Lions (via Kubernetes Secrets) ===
|
||||||
|
# DB_HOST=postgresql-service.postgresql.svc.cluster.local
|
||||||
|
# DB_PORT=5432
|
||||||
|
# DB_NAME=mic-after-work-server-impl-quarkus-main
|
||||||
|
# DB_USERNAME=lionsuser
|
||||||
|
# DB_PASSWORD=<voir-kubernetes-secrets>
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# JWT / SÉCURITÉ
|
||||||
|
# ============================================
|
||||||
|
# Secret pour signer les tokens JWT (minimum 32 caractères)
|
||||||
|
# Générez avec: openssl rand -base64 32
|
||||||
|
JWT_SECRET=afterwork-jwt-secret-min-32-bytes-for-hs256!
|
||||||
|
JWT_LIFESPAN=86400
|
||||||
|
JWT_ISSUER=afterwork-api
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# SUPER ADMIN
|
||||||
|
# ============================================
|
||||||
|
SUPER_ADMIN_EMAIL=superadmin@afterwork.lions.dev
|
||||||
|
SUPER_ADMIN_PASSWORD=SuperAdmin2025!
|
||||||
|
SUPER_ADMIN_API_KEY=dev-super-admin-key
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# EMAIL (SMTP)
|
||||||
|
# ============================================
|
||||||
|
# Mode mock pour le développement (pas d'envoi réel)
|
||||||
|
MAILER_MOCK=true
|
||||||
|
MAILER_HOST=smtp.gmail.com
|
||||||
|
MAILER_PORT=587
|
||||||
|
MAILER_USERNAME=noreply@afterwork.ci
|
||||||
|
MAILER_PASSWORD=CHANGEZ_MOI_SMTP_PASSWORD
|
||||||
|
MAILER_FROM=AfterWork <noreply@afterwork.ci>
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# KAFKA
|
||||||
|
# ============================================
|
||||||
|
# === Développement local ===
|
||||||
|
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
|
||||||
|
|
||||||
|
# === Production Lions ===
|
||||||
|
# KAFKA_BOOTSTRAP_SERVERS=kafka-service.kafka.svc.cluster.local:9092
|
||||||
|
|
||||||
|
# === Confluent Cloud (optionnel) ===
|
||||||
|
# KAFKA_BOOTSTRAP_SERVERS=pkc-xxxxx.region.provider.confluent.cloud:9092
|
||||||
|
# KAFKA_SECURITY_PROTOCOL=SASL_SSL
|
||||||
|
# KAFKA_SASL_MECHANISM=PLAIN
|
||||||
|
# KAFKA_SASL_USERNAME=YOUR_API_KEY
|
||||||
|
# KAFKA_SASL_PASSWORD=YOUR_API_SECRET
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# WAVE PAYMENT
|
||||||
|
# ============================================
|
||||||
|
WAVE_BASE_URL=https://api.wave.com
|
||||||
|
WAVE_API_KEY=VOTRE_CLE_API_WAVE
|
||||||
|
WAVE_SECRET=VOTRE_SECRET_WAVE
|
||||||
|
WAVE_CURRENCY=XOF
|
||||||
|
WAVE_CALLBACK_URL=https://api.lions.dev/afterwork/webhooks/wave
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# RATE LIMITING
|
||||||
|
# ============================================
|
||||||
|
AFTERWORK_RATELIMIT_MAX_REQUESTS=10
|
||||||
|
AFTERWORK_RATELIMIT_WINDOW_SECONDS=60
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# QUARKUS
|
||||||
|
# ============================================
|
||||||
|
QUARKUS_PROFILE=dev
|
||||||
|
QUARKUS_PACKAGE_TYPE=fast-jar
|
||||||
|
QUARKUS_LOG_LEVEL=INFO
|
||||||
|
QUARKUS_LOG_CONSOLE_JSON=false
|
||||||
|
|
||||||
|
# CORS (développement)
|
||||||
|
QUARKUS_HTTP_CORS=true
|
||||||
|
QUARKUS_HTTP_CORS_ORIGINS=http://localhost:3000,http://localhost:4200
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# OBSERVABILITÉ
|
||||||
|
# ============================================
|
||||||
|
# Métriques Prometheus (auto-découverte via annotations K8s)
|
||||||
|
QUARKUS_MICROMETER_EXPORT_PROMETHEUS_ENABLED=true
|
||||||
|
|
||||||
|
# Health checks
|
||||||
|
QUARKUS_SMALLRYE_HEALTH_UI_ENABLE=true
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# DÉPLOIEMENT LIONS (lionsctl)
|
||||||
|
# ============================================
|
||||||
|
# Pour déployer avec lionsctl:
|
||||||
|
# lionsctl pipeline \
|
||||||
|
# -u https://git.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main \
|
||||||
|
# -b develop \
|
||||||
|
# -j 17 \
|
||||||
|
# -e production \
|
||||||
|
# -c k2 \
|
||||||
|
# -m dadyo@lions.dev
|
||||||
|
|
||||||
|
# Variables d'environnement requises pour lionsctl:
|
||||||
|
# LIONS_REGISTRY_USERNAME=lionsregistry
|
||||||
|
# LIONS_REGISTRY_PASSWORD=<votre-mot-de-passe>
|
||||||
|
# LIONS_GITEA_USERNAME=lionsctl-bot
|
||||||
|
# LIONS_GITEA_PASSWORD=lionsctl-bot@2025
|
||||||
@@ -1,12 +1,2 @@
|
|||||||
apiVersion: v1
|
# ConfigMap déplacé dans afterwork-secrets.yaml pour cohérence
|
||||||
kind: ConfigMap
|
# Voir afterwork-secrets.yaml pour la configuration complète
|
||||||
metadata:
|
|
||||||
name: afterwork-config
|
|
||||||
namespace: applications
|
|
||||||
data:
|
|
||||||
DB_HOST: "postgresql"
|
|
||||||
DB_PORT: "5432"
|
|
||||||
DB_NAME: "afterwork_db"
|
|
||||||
DB_USERNAME: "afterwork"
|
|
||||||
QUARKUS_PROFILE: "prod"
|
|
||||||
TZ: "Africa/Douala"
|
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: afterwork-api
|
name: mic-after-work-server-impl-quarkus-main
|
||||||
namespace: applications
|
namespace: applications
|
||||||
labels:
|
labels:
|
||||||
app: afterwork-api
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
environment: production
|
environment: production
|
||||||
|
component: application
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
annotations:
|
||||||
|
description: "AfterWork API - Application sociale déployée via lionsctl"
|
||||||
|
lionsctl.lions.dev/deployed-by: "lionsctl"
|
||||||
spec:
|
spec:
|
||||||
replicas: 2
|
replicas: 1
|
||||||
|
revisionHistoryLimit: 3
|
||||||
strategy:
|
strategy:
|
||||||
type: RollingUpdate
|
type: RollingUpdate
|
||||||
rollingUpdate:
|
rollingUpdate:
|
||||||
@@ -16,37 +22,86 @@ spec:
|
|||||||
maxUnavailable: 0
|
maxUnavailable: 0
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: afterwork-api
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: afterwork-api
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
component: application
|
||||||
|
project: lions-infrastructure-2025
|
||||||
annotations:
|
annotations:
|
||||||
|
# Prometheus scraping - Lions Prometheus auto-découvre via ces annotations
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "8080"
|
prometheus.io/port: "8080"
|
||||||
prometheus.io/path: "/afterwork/q/metrics"
|
prometheus.io/path: "/afterwork/q/metrics"
|
||||||
spec:
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
fsGroup: 1001
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
containers:
|
containers:
|
||||||
- name: afterwork-api
|
- name: mic-after-work-server-impl-quarkus-main
|
||||||
image: registry.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main:d659416
|
image: registry.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
|
# Variables d'environnement depuis ConfigMap et Secrets
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: afterwork-config
|
name: afterwork-config
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: afterwork-secrets
|
name: afterwork-secrets
|
||||||
|
env:
|
||||||
|
# Override explicites pour Quarkus
|
||||||
|
- name: QUARKUS_DATASOURCE_DB_KIND
|
||||||
|
value: "postgresql"
|
||||||
|
- name: QUARKUS_DATASOURCE_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: afterwork-config
|
||||||
|
key: DB_USERNAME
|
||||||
|
- name: QUARKUS_DATASOURCE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: afterwork-secrets
|
||||||
|
key: DB_PASSWORD
|
||||||
|
- name: QUARKUS_DATASOURCE_JDBC_URL
|
||||||
|
value: "jdbc:postgresql://$(DB_HOST):$(DB_PORT)/$(DB_NAME)"
|
||||||
|
# Kafka - Lions Kafka cluster
|
||||||
|
- name: KAFKA_BOOTSTRAP_SERVERS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: afterwork-config
|
||||||
|
key: KAFKA_BOOTSTRAP_SERVERS
|
||||||
|
# JWT
|
||||||
|
- name: SMALLRYE_JWT_SIGN_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: afterwork-secrets
|
||||||
|
key: JWT_SECRET
|
||||||
|
- name: MP_JWT_VERIFY_ISSUER
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: afterwork-config
|
||||||
|
key: JWT_ISSUER
|
||||||
|
# Java options
|
||||||
|
- name: JAVA_OPTS
|
||||||
|
value: "-Xms256m -Xmx512m -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "512Mi"
|
memory: "512Mi"
|
||||||
cpu: "250m"
|
cpu: "200m"
|
||||||
limits:
|
limits:
|
||||||
memory: "1Gi"
|
memory: "1Gi"
|
||||||
cpu: "1000m"
|
cpu: "1000m"
|
||||||
|
# Health checks HTTP (utilisent les endpoints SmallRye Health)
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /afterwork/q/health/live
|
path: /afterwork/q/health/live
|
||||||
@@ -67,13 +122,35 @@ spec:
|
|||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
successThreshold: 1
|
successThreshold: 1
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
|
# Startup probe pour éviter les kills pendant le démarrage
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /afterwork/q/health/started
|
||||||
|
port: 8080
|
||||||
|
scheme: HTTP
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 30
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1001
|
||||||
|
runAsGroup: 1001
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: temp-uploads
|
- name: tmp-volume
|
||||||
mountPath: /tmp/uploads
|
mountPath: /tmp
|
||||||
|
- name: logs-volume
|
||||||
|
mountPath: /app/logs
|
||||||
volumes:
|
volumes:
|
||||||
- name: temp-uploads
|
- name: tmp-volume
|
||||||
emptyDir:
|
emptyDir: {}
|
||||||
sizeLimit: 1Gi
|
- name: logs-volume
|
||||||
|
emptyDir: {}
|
||||||
imagePullSecrets:
|
imagePullSecrets:
|
||||||
- name: lionsregistry-secret
|
- name: lionsregistry-secret
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
|
|||||||
408
kubernetes/afterwork-monitoring.yaml
Normal file
408
kubernetes/afterwork-monitoring.yaml
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# AfterWork API - Configuration Monitoring pour Lions Infrastructure
|
||||||
|
# ==============================================================================
|
||||||
|
# Cette configuration intègre l'application avec:
|
||||||
|
# - Prometheus (https://prometheus.lions.dev) - scraping auto via annotations
|
||||||
|
# - Grafana (https://grafana.lions.dev) - dashboard dédié
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
---
|
||||||
|
# ==============================================================================
|
||||||
|
# ServiceMonitor pour Prometheus Operator (si installé)
|
||||||
|
# ==============================================================================
|
||||||
|
# Note: L'infrastructure Lions utilise le scraping via annotations pod, mais
|
||||||
|
# ce ServiceMonitor peut être utilisé si Prometheus Operator est déployé.
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: afterwork-api-monitor
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
|
release: prometheus
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- applications
|
||||||
|
endpoints:
|
||||||
|
- port: http-direct
|
||||||
|
path: /afterwork/q/metrics
|
||||||
|
interval: 30s
|
||||||
|
scrapeTimeout: 10s
|
||||||
|
scheme: http
|
||||||
|
|
||||||
|
---
|
||||||
|
# ==============================================================================
|
||||||
|
# PrometheusRule - Alertes pour AfterWork API
|
||||||
|
# ==============================================================================
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: PrometheusRule
|
||||||
|
metadata:
|
||||||
|
name: afterwork-api-alerts
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
|
release: prometheus
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
spec:
|
||||||
|
groups:
|
||||||
|
- name: afterwork-api.rules
|
||||||
|
rules:
|
||||||
|
# Alerte si l'application est down
|
||||||
|
- alert: AfterWorkAPIDown
|
||||||
|
expr: up{job=~".*afterwork.*"} == 0
|
||||||
|
for: 2m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
application: afterwork-api
|
||||||
|
annotations:
|
||||||
|
summary: "AfterWork API is down"
|
||||||
|
description: "L'API AfterWork n'est pas accessible depuis plus de 2 minutes"
|
||||||
|
|
||||||
|
# Alerte si le taux d'erreur HTTP 5xx est élevé
|
||||||
|
- alert: AfterWorkHighErrorRate
|
||||||
|
expr: |
|
||||||
|
sum(rate(http_server_requests_seconds_count{
|
||||||
|
kubernetes_namespace="applications",
|
||||||
|
app="mic-after-work-server-impl-quarkus-main",
|
||||||
|
status=~"5.."
|
||||||
|
}[5m])) /
|
||||||
|
sum(rate(http_server_requests_seconds_count{
|
||||||
|
kubernetes_namespace="applications",
|
||||||
|
app="mic-after-work-server-impl-quarkus-main"
|
||||||
|
}[5m])) > 0.05
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
application: afterwork-api
|
||||||
|
annotations:
|
||||||
|
summary: "High error rate on AfterWork API"
|
||||||
|
description: "Le taux d'erreur 5xx est supérieur à 5% depuis 5 minutes"
|
||||||
|
|
||||||
|
# Alerte si la latence p95 est élevée
|
||||||
|
- alert: AfterWorkHighLatency
|
||||||
|
expr: |
|
||||||
|
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{
|
||||||
|
kubernetes_namespace="applications",
|
||||||
|
app="mic-after-work-server-impl-quarkus-main"
|
||||||
|
}[5m])) by (le)) > 2
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
application: afterwork-api
|
||||||
|
annotations:
|
||||||
|
summary: "High latency on AfterWork API"
|
||||||
|
description: "La latence p95 dépasse 2 secondes depuis 5 minutes"
|
||||||
|
|
||||||
|
# Alerte si la mémoire est proche de la limite
|
||||||
|
- alert: AfterWorkHighMemoryUsage
|
||||||
|
expr: |
|
||||||
|
sum(container_memory_working_set_bytes{
|
||||||
|
namespace="applications",
|
||||||
|
pod=~"mic-after-work-server-impl-quarkus-main.*"
|
||||||
|
}) /
|
||||||
|
sum(container_spec_memory_limit_bytes{
|
||||||
|
namespace="applications",
|
||||||
|
pod=~"mic-after-work-server-impl-quarkus-main.*"
|
||||||
|
}) > 0.85
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
application: afterwork-api
|
||||||
|
annotations:
|
||||||
|
summary: "High memory usage on AfterWork API"
|
||||||
|
description: "L'utilisation mémoire dépasse 85% de la limite"
|
||||||
|
|
||||||
|
# Alerte si le pod redémarre fréquemment
|
||||||
|
- alert: AfterWorkPodRestarts
|
||||||
|
expr: |
|
||||||
|
increase(kube_pod_container_status_restarts_total{
|
||||||
|
namespace="applications",
|
||||||
|
pod=~"mic-after-work-server-impl-quarkus-main.*"
|
||||||
|
}[1h]) > 3
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
application: afterwork-api
|
||||||
|
annotations:
|
||||||
|
summary: "AfterWork API pod restarting frequently"
|
||||||
|
description: "Le pod a redémarré plus de 3 fois dans la dernière heure"
|
||||||
|
|
||||||
|
---
|
||||||
|
# ==============================================================================
|
||||||
|
# Grafana Dashboard ConfigMap (pour import automatique)
|
||||||
|
# ==============================================================================
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: afterwork-grafana-dashboard
|
||||||
|
namespace: monitoring
|
||||||
|
labels:
|
||||||
|
grafana_dashboard: "1"
|
||||||
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
data:
|
||||||
|
afterwork-api-dashboard.json: |
|
||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{"color": "green", "value": null},
|
||||||
|
{"color": "yellow", "value": 100},
|
||||||
|
{"color": "red", "value": 500}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "reqps"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
|
||||||
|
"id": 1,
|
||||||
|
"options": {},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m]))",
|
||||||
|
"legendFormat": "Requests/s",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Request Rate",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"unit": "ms"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
||||||
|
"id": 2,
|
||||||
|
"options": {},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) by (le)) * 1000",
|
||||||
|
"legendFormat": "p95 Latency",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "histogram_quantile(0.50, sum(rate(http_server_requests_seconds_bucket{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) by (le)) * 1000",
|
||||||
|
"legendFormat": "p50 Latency",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Response Time",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"unit": "percent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
|
||||||
|
"id": 3,
|
||||||
|
"options": {},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) * 100",
|
||||||
|
"legendFormat": "Error Rate %",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Error Rate",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"unit": "bytes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
|
||||||
|
"id": 4,
|
||||||
|
"options": {},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(container_memory_working_set_bytes{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"})",
|
||||||
|
"legendFormat": "Memory Used",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "sum(container_spec_memory_limit_bytes{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"})",
|
||||||
|
"legendFormat": "Memory Limit",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Memory Usage",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
|
||||||
|
"id": 5,
|
||||||
|
"options": {},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"}[5m])) * 1000",
|
||||||
|
"legendFormat": "CPU Usage (millicores)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "CPU Usage",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{"color": "red", "value": null},
|
||||||
|
{"color": "green", "value": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 16},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showThresholdLabels": false,
|
||||||
|
"showThresholdMarkers": true
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "up{job=~\".*afterwork.*\"}",
|
||||||
|
"legendFormat": "Status",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "API Status",
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheus"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{"color": "green", "value": null},
|
||||||
|
{"color": "yellow", "value": 1},
|
||||||
|
{"color": "red", "value": 3}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 16},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["lastNotNull"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "increase(kube_pod_container_status_restarts_total{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"}[1h])",
|
||||||
|
"legendFormat": "Restarts (1h)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Pod Restarts (1h)",
|
||||||
|
"type": "stat"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "30s",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": ["lions", "afterwork", "quarkus", "api"],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "AfterWork API Dashboard",
|
||||||
|
"uid": "afterwork-api",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
@@ -6,8 +6,175 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
app: afterwork-api
|
app: afterwork-api
|
||||||
component: secrets
|
component: secrets
|
||||||
|
environment: production
|
||||||
|
project: lions-infrastructure-2025
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
# Base de données PostgreSQL
|
# ==============================================================================
|
||||||
# Pattern cohérent avec unionflow et btpxpress
|
# BASE DE DONNÉES PostgreSQL
|
||||||
|
# ==============================================================================
|
||||||
|
# Utilise le PostgreSQL de l'infrastructure Lions
|
||||||
|
# postgresql-service.postgresql.svc.cluster.local:5432
|
||||||
DB_PASSWORD: "AfterWork2025!"
|
DB_PASSWORD: "AfterWork2025!"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# JWT / SÉCURITÉ
|
||||||
|
# ==============================================================================
|
||||||
|
# Clé secrète JWT (minimum 32 caractères, aléatoire)
|
||||||
|
# Générer avec: openssl rand -base64 32
|
||||||
|
JWT_SECRET: "AfterWorkJWTSecret2025LionsInfrastructureKey"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# COMPTE ADMINISTRATEUR INITIAL
|
||||||
|
# ==============================================================================
|
||||||
|
ADMIN_EMAIL: "admin@afterwork.ci"
|
||||||
|
ADMIN_PASSWORD: "AdminAfterWork2025!"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SERVICE EMAIL (SMTP)
|
||||||
|
# ==============================================================================
|
||||||
|
# Configuration Gmail ou autre SMTP
|
||||||
|
MAILER_USERNAME: "noreply@afterwork.ci"
|
||||||
|
MAILER_PASSWORD: "CHANGEZ_MOI_SMTP_PASSWORD"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# WAVE PAYMENT (Intégration paiement)
|
||||||
|
# ==============================================================================
|
||||||
|
WAVE_API_KEY: "CHANGEZ_MOI_WAVE_API_KEY"
|
||||||
|
WAVE_SECRET: "CHANGEZ_MOI_WAVE_SECRET"
|
||||||
|
|
||||||
|
---
|
||||||
|
# ==============================================================================
|
||||||
|
# CONFIGMAP POUR CONFIGURATION NON-SENSIBLE
|
||||||
|
# ==============================================================================
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: afterwork-config
|
||||||
|
namespace: applications
|
||||||
|
labels:
|
||||||
|
app: afterwork-api
|
||||||
|
component: configuration
|
||||||
|
environment: production
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
data:
|
||||||
|
# ==============================================================================
|
||||||
|
# BASE DE DONNÉES - Lions PostgreSQL
|
||||||
|
# ==============================================================================
|
||||||
|
DB_HOST: "postgresql-service.postgresql.svc.cluster.local"
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "mic-after-work-server-impl-quarkus-main"
|
||||||
|
DB_USERNAME: "lionsuser"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# QUARKUS
|
||||||
|
# ==============================================================================
|
||||||
|
QUARKUS_PROFILE: "prod"
|
||||||
|
QUARKUS_LOG_LEVEL: "INFO"
|
||||||
|
QUARKUS_LOG_CONSOLE_JSON: "true"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# JWT
|
||||||
|
# ==============================================================================
|
||||||
|
JWT_LIFESPAN: "86400"
|
||||||
|
JWT_ISSUER: "afterwork-api"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# KAFKA - Lions Infrastructure
|
||||||
|
# ==============================================================================
|
||||||
|
# Utilise le Kafka déployé dans le namespace kafka
|
||||||
|
KAFKA_BOOTSTRAP_SERVERS: "kafka-service.kafka.svc.cluster.local:9092"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# EMAIL (SMTP)
|
||||||
|
# ==============================================================================
|
||||||
|
MAILER_HOST: "smtp.gmail.com"
|
||||||
|
MAILER_PORT: "587"
|
||||||
|
MAILER_FROM: "AfterWork <noreply@afterwork.ci>"
|
||||||
|
MAILER_START_TLS: "REQUIRED"
|
||||||
|
# En production, mettre false. true = mock (pas d'envoi réel)
|
||||||
|
MAILER_MOCK: "true"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# RATE LIMITING
|
||||||
|
# ==============================================================================
|
||||||
|
AFTERWORK_RATELIMIT_MAX_REQUESTS: "10"
|
||||||
|
AFTERWORK_RATELIMIT_WINDOW_SECONDS: "60"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# WAVE PAYMENT
|
||||||
|
# ==============================================================================
|
||||||
|
WAVE_BASE_URL: "https://api.wave.com"
|
||||||
|
WAVE_CURRENCY: "XOF"
|
||||||
|
WAVE_CALLBACK_URL: "https://api.lions.dev/afterwork/webhooks/wave"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# OBSERVABILITY - Lions Prometheus/Grafana
|
||||||
|
# ==============================================================================
|
||||||
|
# Prometheus scrape via annotations sur le pod
|
||||||
|
# Grafana disponible sur https://grafana.lions.dev
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# KEYCLOAK / SSO (optionnel)
|
||||||
|
# ==============================================================================
|
||||||
|
# OIDC_AUTH_SERVER_URL: "https://security.lions.dev/realms/lions"
|
||||||
|
# OIDC_CLIENT_ID: "afterwork-api"
|
||||||
|
|
||||||
|
---
|
||||||
|
# ==============================================================================
|
||||||
|
# EXTERNAL SECRET - Intégration Vault (ACTIF)
|
||||||
|
# ==============================================================================
|
||||||
|
# Vault est déverrouillé sur https://vault.lions.dev
|
||||||
|
# Les secrets sont synchronisés depuis Vault vers Kubernetes automatiquement
|
||||||
|
#
|
||||||
|
# PRÉREQUIS: Créer les secrets dans Vault avec:
|
||||||
|
# vault kv put lions/afterwork \
|
||||||
|
# db_password="AfterWork2025!" \
|
||||||
|
# jwt_secret="AfterWorkJWTSecret2025LionsInfrastructureKey" \
|
||||||
|
# admin_password="AdminAfterWork2025!" \
|
||||||
|
# mailer_password="SMTP_PASSWORD" \
|
||||||
|
# wave_api_key="WAVE_KEY" \
|
||||||
|
# wave_secret="WAVE_SECRET"
|
||||||
|
#
|
||||||
|
apiVersion: external-secrets.io/v1beta1
|
||||||
|
kind: ExternalSecret
|
||||||
|
metadata:
|
||||||
|
name: afterwork-vault-secrets
|
||||||
|
namespace: applications
|
||||||
|
labels:
|
||||||
|
app: afterwork-api
|
||||||
|
component: external-secrets
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
spec:
|
||||||
|
refreshInterval: "1h"
|
||||||
|
secretStoreRef:
|
||||||
|
name: vault-backend
|
||||||
|
kind: ClusterSecretStore
|
||||||
|
target:
|
||||||
|
name: afterwork-secrets-vault
|
||||||
|
creationPolicy: Owner
|
||||||
|
data:
|
||||||
|
- secretKey: DB_PASSWORD
|
||||||
|
remoteRef:
|
||||||
|
key: lions/data/afterwork
|
||||||
|
property: db_password
|
||||||
|
- secretKey: JWT_SECRET
|
||||||
|
remoteRef:
|
||||||
|
key: lions/data/afterwork
|
||||||
|
property: jwt_secret
|
||||||
|
- secretKey: ADMIN_PASSWORD
|
||||||
|
remoteRef:
|
||||||
|
key: lions/data/afterwork
|
||||||
|
property: admin_password
|
||||||
|
- secretKey: MAILER_PASSWORD
|
||||||
|
remoteRef:
|
||||||
|
key: lions/data/afterwork
|
||||||
|
property: mailer_password
|
||||||
|
- secretKey: WAVE_API_KEY
|
||||||
|
remoteRef:
|
||||||
|
key: lions/data/afterwork
|
||||||
|
property: wave_api_key
|
||||||
|
- secretKey: WAVE_SECRET
|
||||||
|
remoteRef:
|
||||||
|
key: lions/data/afterwork
|
||||||
|
property: wave_secret
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: afterwork-api
|
name: mic-after-work-server-impl-quarkus-main-service
|
||||||
namespace: applications
|
namespace: applications
|
||||||
labels:
|
labels:
|
||||||
app: afterwork-api
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
|
component: application
|
||||||
|
project: lions-infrastructure-2025
|
||||||
|
annotations:
|
||||||
|
description: "Service for AfterWork API"
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
sessionAffinity: ClientIP
|
sessionAffinity: ClientIP
|
||||||
@@ -12,9 +16,15 @@ spec:
|
|||||||
clientIP:
|
clientIP:
|
||||||
timeoutSeconds: 10800
|
timeoutSeconds: 10800
|
||||||
ports:
|
ports:
|
||||||
- port: 8080
|
# Port 80 exposé, route vers 8080 du container
|
||||||
|
- port: 80
|
||||||
targetPort: 8080
|
targetPort: 8080
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
|
# Port 8080 pour compatibilité directe
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
name: http-direct
|
||||||
selector:
|
selector:
|
||||||
app: afterwork-api
|
app: mic-after-work-server-impl-quarkus-main
|
||||||
|
|||||||
24
pom.xml
24
pom.xml
@@ -126,6 +126,25 @@
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-mailer</artifactId>
|
<artifactId>quarkus-mailer</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<!-- HEALTH CHECKS & OBSERVABILITY -->
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-health</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-micrometer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ============================================== -->
|
||||||
|
<!-- TEST DEPENDENCIES -->
|
||||||
|
<!-- ============================================== -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-junit5</artifactId>
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
@@ -136,6 +155,11 @@
|
|||||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-test-security</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.rest-assured</groupId>
|
<groupId>io.rest-assured</groupId>
|
||||||
<artifactId>rest-assured</artifactId>
|
<artifactId>rest-assured</artifactId>
|
||||||
|
|||||||
110
src/main/java/com/lions/dev/exception/GlobalExceptionMapper.java
Normal file
110
src/main/java/com/lions/dev/exception/GlobalExceptionMapper.java
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package com.lions.dev.exception;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import jakarta.ws.rs.ForbiddenException;
|
||||||
|
import jakarta.ws.rs.NotAuthorizedException;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
import jakarta.ws.rs.WebApplicationException;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||||
|
import jakarta.ws.rs.ext.Provider;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper d'exceptions global pour standardiser les réponses d'erreur de l'API.
|
||||||
|
*
|
||||||
|
* Format de réponse d'erreur standard:
|
||||||
|
* {
|
||||||
|
* "timestamp": "2026-02-05T12:00:00Z",
|
||||||
|
* "status": 400,
|
||||||
|
* "error": "Bad Request",
|
||||||
|
* "message": "Description de l'erreur",
|
||||||
|
* "path": "/api/endpoint"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @since 2.0 - Production-ready
|
||||||
|
*/
|
||||||
|
@Provider
|
||||||
|
public class GlobalExceptionMapper implements ExceptionMapper<Throwable> {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(GlobalExceptionMapper.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Throwable exception) {
|
||||||
|
// Déterminer le statut et le message en fonction du type d'exception
|
||||||
|
int status;
|
||||||
|
String error;
|
||||||
|
String message;
|
||||||
|
|
||||||
|
if (exception instanceof ConstraintViolationException cve) {
|
||||||
|
status = Response.Status.BAD_REQUEST.getStatusCode();
|
||||||
|
error = "Validation Error";
|
||||||
|
message = cve.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
LOG.warnf("Validation error: %s", message);
|
||||||
|
} else if (exception instanceof IllegalArgumentException) {
|
||||||
|
status = Response.Status.BAD_REQUEST.getStatusCode();
|
||||||
|
error = "Bad Request";
|
||||||
|
message = exception.getMessage();
|
||||||
|
LOG.warnf("Bad request: %s", message);
|
||||||
|
} else if (exception instanceof EntityNotFoundException || exception instanceof NotFoundException) {
|
||||||
|
status = Response.Status.NOT_FOUND.getStatusCode();
|
||||||
|
error = "Not Found";
|
||||||
|
message = exception.getMessage() != null ? exception.getMessage() : "Resource not found";
|
||||||
|
LOG.warnf("Not found: %s", message);
|
||||||
|
} else if (exception instanceof UserNotFoundException) {
|
||||||
|
status = Response.Status.NOT_FOUND.getStatusCode();
|
||||||
|
error = "User Not Found";
|
||||||
|
message = exception.getMessage();
|
||||||
|
LOG.warnf("User not found: %s", message);
|
||||||
|
} else if (exception instanceof NotAuthorizedException) {
|
||||||
|
status = Response.Status.UNAUTHORIZED.getStatusCode();
|
||||||
|
error = "Unauthorized";
|
||||||
|
message = "Authentication required";
|
||||||
|
LOG.warnf("Unauthorized access attempt");
|
||||||
|
} else if (exception instanceof ForbiddenException || exception instanceof UnauthorizedException) {
|
||||||
|
status = Response.Status.FORBIDDEN.getStatusCode();
|
||||||
|
error = "Forbidden";
|
||||||
|
message = exception.getMessage() != null ? exception.getMessage() : "Access denied";
|
||||||
|
LOG.warnf("Forbidden: %s", message);
|
||||||
|
} else if (exception instanceof SecurityException) {
|
||||||
|
status = Response.Status.FORBIDDEN.getStatusCode();
|
||||||
|
error = "Security Error";
|
||||||
|
message = exception.getMessage();
|
||||||
|
LOG.warnf("Security error: %s", message);
|
||||||
|
} else if (exception instanceof WebApplicationException wae) {
|
||||||
|
Response response = wae.getResponse();
|
||||||
|
status = response.getStatus();
|
||||||
|
error = Response.Status.fromStatusCode(status).getReasonPhrase();
|
||||||
|
message = exception.getMessage();
|
||||||
|
LOG.warnf("Web application exception (%d): %s", status, message);
|
||||||
|
} else {
|
||||||
|
// Erreur interne non gérée
|
||||||
|
status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
|
||||||
|
error = "Internal Server Error";
|
||||||
|
message = "An unexpected error occurred. Please try again later.";
|
||||||
|
LOG.errorf(exception, "Unhandled exception: %s", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire la réponse d'erreur standardisée
|
||||||
|
Map<String, Object> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("timestamp", Instant.now().toString());
|
||||||
|
errorResponse.put("status", status);
|
||||||
|
errorResponse.put("error", error);
|
||||||
|
errorResponse.put("message", message);
|
||||||
|
|
||||||
|
return Response.status(status)
|
||||||
|
.type(MediaType.APPLICATION_JSON)
|
||||||
|
.entity(errorResponse)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
159
src/main/java/com/lions/dev/filter/RateLimitFilter.java
Normal file
159
src/main/java/com/lions/dev/filter/RateLimitFilter.java
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package com.lions.dev.filter;
|
||||||
|
|
||||||
|
import jakarta.annotation.Priority;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.ws.rs.Priorities;
|
||||||
|
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||||
|
import jakarta.ws.rs.container.ContainerRequestFilter;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.ext.Provider;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre de limitation de débit (rate limiting) pour protéger les endpoints sensibles
|
||||||
|
* contre les attaques par force brute.
|
||||||
|
*
|
||||||
|
* Endpoints protégés:
|
||||||
|
* - /authenticate (login)
|
||||||
|
* - /forgot-password (reset password)
|
||||||
|
* - /register (création de compte)
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - afterwork.ratelimit.max-requests: Nombre max de requêtes par fenêtre (défaut: 10)
|
||||||
|
* - afterwork.ratelimit.window-seconds: Durée de la fenêtre en secondes (défaut: 60)
|
||||||
|
*
|
||||||
|
* @since 2.0 - Production-ready security
|
||||||
|
*/
|
||||||
|
@Provider
|
||||||
|
@Priority(Priorities.AUTHENTICATION - 1)
|
||||||
|
@ApplicationScoped
|
||||||
|
public class RateLimitFilter implements ContainerRequestFilter {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(RateLimitFilter.class);
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.ratelimit.max-requests", defaultValue = "10")
|
||||||
|
int maxRequests;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.ratelimit.window-seconds", defaultValue = "60")
|
||||||
|
int windowSeconds;
|
||||||
|
|
||||||
|
// Stockage des compteurs par IP et endpoint
|
||||||
|
private final ConcurrentHashMap<String, RateLimitEntry> rateLimitCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Endpoints à protéger
|
||||||
|
private static final String[] PROTECTED_ENDPOINTS = {
|
||||||
|
"/authenticate",
|
||||||
|
"/forgot-password",
|
||||||
|
"/register",
|
||||||
|
"/users/authenticate",
|
||||||
|
"/users/register",
|
||||||
|
"/users/forgot-password"
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void filter(ContainerRequestContext requestContext) throws IOException {
|
||||||
|
String path = requestContext.getUriInfo().getPath();
|
||||||
|
|
||||||
|
// Vérifier si l'endpoint est protégé
|
||||||
|
boolean isProtected = false;
|
||||||
|
for (String endpoint : PROTECTED_ENDPOINTS) {
|
||||||
|
if (path.contains(endpoint)) {
|
||||||
|
isProtected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isProtected) {
|
||||||
|
return; // Pas de rate limiting pour cet endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientIp = getClientIp(requestContext);
|
||||||
|
String key = clientIp + ":" + path;
|
||||||
|
|
||||||
|
RateLimitEntry entry = rateLimitCache.compute(key, (k, existing) -> {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
if (existing == null || (now - existing.windowStart) >= windowSeconds) {
|
||||||
|
// Nouvelle fenêtre
|
||||||
|
return new RateLimitEntry(now, 1);
|
||||||
|
} else {
|
||||||
|
// Même fenêtre, incrémenter
|
||||||
|
existing.incrementCount();
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entry.getCount() > maxRequests) {
|
||||||
|
LOG.warnf("Rate limit exceeded for IP: %s on endpoint: %s (count: %d)",
|
||||||
|
clientIp, path, entry.getCount());
|
||||||
|
|
||||||
|
requestContext.abortWith(
|
||||||
|
Response.status(429)
|
||||||
|
.entity("{\"message\": \"Trop de requêtes. Veuillez réessayer dans " + windowSeconds + " secondes.\"}")
|
||||||
|
.header("Retry-After", String.valueOf(windowSeconds))
|
||||||
|
.header("X-RateLimit-Limit", String.valueOf(maxRequests))
|
||||||
|
.header("X-RateLimit-Remaining", "0")
|
||||||
|
.header("X-RateLimit-Reset", String.valueOf(entry.windowStart + windowSeconds))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait l'IP du client en tenant compte des proxies.
|
||||||
|
*/
|
||||||
|
private String getClientIp(ContainerRequestContext context) {
|
||||||
|
// Vérifier X-Forwarded-For pour les proxies/load balancers
|
||||||
|
String xff = context.getHeaderString("X-Forwarded-For");
|
||||||
|
if (xff != null && !xff.isBlank()) {
|
||||||
|
// Prendre la première IP (client original)
|
||||||
|
return xff.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier X-Real-IP (nginx)
|
||||||
|
String realIp = context.getHeaderString("X-Real-IP");
|
||||||
|
if (realIp != null && !realIp.isBlank()) {
|
||||||
|
return realIp.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: adresse inconnue (ne devrait pas arriver en production)
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie les entrées expirées du cache (à appeler périodiquement).
|
||||||
|
*/
|
||||||
|
public void cleanupExpiredEntries() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
rateLimitCache.entrySet().removeIf(entry ->
|
||||||
|
(now - entry.getValue().windowStart) >= windowSeconds * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classe interne pour stocker les informations de rate limiting.
|
||||||
|
*/
|
||||||
|
private static class RateLimitEntry {
|
||||||
|
final long windowStart;
|
||||||
|
private final AtomicInteger count;
|
||||||
|
|
||||||
|
RateLimitEntry(long windowStart, int initialCount) {
|
||||||
|
this.windowStart = windowStart;
|
||||||
|
this.count = new AtomicInteger(initialCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void incrementCount() {
|
||||||
|
count.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
int getCount() {
|
||||||
|
return count.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/main/java/com/lions/dev/health/KafkaHealthCheck.java
Normal file
54
src/main/java/com/lions/dev/health/KafkaHealthCheck.java
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package com.lions.dev.health;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheck;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
||||||
|
import org.eclipse.microprofile.health.Readiness;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check pour la connexion Kafka.
|
||||||
|
* Vérifie que le broker Kafka est accessible.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Production-ready
|
||||||
|
*/
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class KafkaHealthCheck implements HealthCheck {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "kafka.bootstrap.servers", defaultValue = "localhost:9092")
|
||||||
|
String bootstrapServers;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("Kafka Connection");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the first bootstrap server
|
||||||
|
String[] servers = bootstrapServers.split(",");
|
||||||
|
String[] hostPort = servers[0].trim().split(":");
|
||||||
|
String host = hostPort[0];
|
||||||
|
int port = hostPort.length > 1 ? Integer.parseInt(hostPort[1]) : 9092;
|
||||||
|
|
||||||
|
// Try to connect
|
||||||
|
try (Socket socket = new Socket()) {
|
||||||
|
socket.connect(new InetSocketAddress(host, port), 2000);
|
||||||
|
responseBuilder.up()
|
||||||
|
.withData("bootstrapServers", bootstrapServers)
|
||||||
|
.withData("status", "Kafka broker is reachable");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
responseBuilder.down()
|
||||||
|
.withData("bootstrapServers", bootstrapServers)
|
||||||
|
.withData("error", e.getMessage())
|
||||||
|
.withData("status", "Kafka broker is NOT reachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseBuilder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/main/java/com/lions/dev/health/LivenessCheck.java
Normal file
28
src/main/java/com/lions/dev/health/LivenessCheck.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.lions.dev.health;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheck;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||||
|
import org.eclipse.microprofile.health.Liveness;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check de vivacité (liveness) pour Kubernetes.
|
||||||
|
* Vérifie que l'application est vivante et répond.
|
||||||
|
*
|
||||||
|
* Endpoint: GET /q/health/live
|
||||||
|
*
|
||||||
|
* @since 2.0 - Production-ready
|
||||||
|
*/
|
||||||
|
@Liveness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class LivenessCheck implements HealthCheck {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
return HealthCheckResponse.named("AfterWork API Liveness")
|
||||||
|
.up()
|
||||||
|
.withData("version", "2.0")
|
||||||
|
.withData("status", "Application is running")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/java/com/lions/dev/health/ReadinessCheck.java
Normal file
46
src/main/java/com/lions/dev/health/ReadinessCheck.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.lions.dev.health;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheck;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponse;
|
||||||
|
import org.eclipse.microprofile.health.HealthCheckResponseBuilder;
|
||||||
|
import org.eclipse.microprofile.health.Readiness;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check de disponibilité (readiness) pour Kubernetes.
|
||||||
|
* Vérifie que l'application est prête à recevoir du trafic.
|
||||||
|
* Contrôle notamment la connexion à la base de données.
|
||||||
|
*
|
||||||
|
* Endpoint: GET /q/health/ready
|
||||||
|
*
|
||||||
|
* @since 2.0 - Production-ready
|
||||||
|
*/
|
||||||
|
@Readiness
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ReadinessCheck implements HealthCheck {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EntityManager entityManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckResponse call() {
|
||||||
|
HealthCheckResponseBuilder responseBuilder = HealthCheckResponse.named("AfterWork API Readiness");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test de la connexion à la base de données
|
||||||
|
entityManager.createNativeQuery("SELECT 1").getSingleResult();
|
||||||
|
responseBuilder.up()
|
||||||
|
.withData("database", "connected")
|
||||||
|
.withData("status", "Application is ready to accept traffic");
|
||||||
|
} catch (Exception e) {
|
||||||
|
responseBuilder.down()
|
||||||
|
.withData("database", "disconnected")
|
||||||
|
.withData("error", e.getMessage())
|
||||||
|
.withData("status", "Application is NOT ready - database connection failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseBuilder.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,16 @@ import com.lions.dev.dto.response.establishment.EstablishmentMediaResponseDTO;
|
|||||||
import com.lions.dev.entity.establishment.EstablishmentMedia;
|
import com.lions.dev.entity.establishment.EstablishmentMedia;
|
||||||
import com.lions.dev.entity.establishment.MediaType;
|
import com.lions.dev.entity.establishment.MediaType;
|
||||||
import com.lions.dev.service.EstablishmentMediaService;
|
import com.lions.dev.service.EstablishmentMediaService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
@@ -20,7 +24,10 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ressource REST pour la gestion des médias d'établissements.
|
* Ressource REST pour la gestion des médias d'établissements.
|
||||||
* Cette classe expose des endpoints pour uploader, récupérer et supprimer des médias.
|
*
|
||||||
|
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||||
*/
|
*/
|
||||||
@Path("/establishments/{establishmentId}/media")
|
@Path("/establishments/{establishmentId}/media")
|
||||||
@Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
@Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||||
@@ -35,8 +42,10 @@ public class EstablishmentMediaResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère tous les médias d'un établissement.
|
* Récupère tous les médias d'un établissement.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
@PermitAll
|
||||||
@Operation(summary = "Récupérer tous les médias d'un établissement",
|
@Operation(summary = "Récupérer tous les médias d'un établissement",
|
||||||
description = "Retourne la liste de tous les médias (photos et vidéos) d'un établissement")
|
description = "Retourne la liste de tous les médias (photos et vidéos) d'un établissement")
|
||||||
public Response getEstablishmentMedia(@PathParam("establishmentId") String establishmentId) {
|
public Response getEstablishmentMedia(@PathParam("establishmentId") String establishmentId) {
|
||||||
@@ -64,12 +73,14 @@ public class EstablishmentMediaResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload un nouveau média pour un établissement.
|
* Upload un nouveau média pour un établissement.
|
||||||
* Accepte un body JSON avec les informations du média.
|
* Requiert une authentification (propriétaire ou manager).
|
||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
@Operation(summary = "Uploader un média pour un établissement",
|
@Operation(summary = "Uploader un média pour un établissement",
|
||||||
description = "Upload un nouveau média (photo ou vidéo) pour un établissement")
|
description = "Upload un nouveau média (photo ou vidéo) pour un établissement")
|
||||||
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
public Response uploadMedia(
|
public Response uploadMedia(
|
||||||
@PathParam("establishmentId") String establishmentId,
|
@PathParam("establishmentId") String establishmentId,
|
||||||
@Valid EstablishmentMediaRequestDTO requestDTO,
|
@Valid EstablishmentMediaRequestDTO requestDTO,
|
||||||
@@ -135,12 +146,15 @@ public class EstablishmentMediaResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Supprime un média d'un établissement.
|
* Supprime un média d'un établissement.
|
||||||
|
* Requiert une authentification (propriétaire ou manager).
|
||||||
*/
|
*/
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{mediaId}")
|
@Path("/{mediaId}")
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
@Operation(summary = "Supprimer un média d'un établissement",
|
@Operation(summary = "Supprimer un média d'un établissement",
|
||||||
description = "Supprime un média spécifique d'un établissement")
|
description = "Supprime un média spécifique d'un établissement")
|
||||||
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
public Response deleteMedia(
|
public Response deleteMedia(
|
||||||
@PathParam("establishmentId") String establishmentId,
|
@PathParam("establishmentId") String establishmentId,
|
||||||
@PathParam("mediaId") String mediaId) {
|
@PathParam("mediaId") String mediaId) {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
package com.lions.dev.resource;
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
import com.lions.dev.core.security.JwtAuthFilter;
|
|
||||||
import com.lions.dev.core.security.RequiresAuth;
|
|
||||||
import com.lions.dev.dto.request.establishment.EstablishmentRatingRequestDTO;
|
import com.lions.dev.dto.request.establishment.EstablishmentRatingRequestDTO;
|
||||||
import com.lions.dev.dto.response.establishment.EstablishmentRatingResponseDTO;
|
import com.lions.dev.dto.response.establishment.EstablishmentRatingResponseDTO;
|
||||||
import com.lions.dev.dto.response.establishment.EstablishmentRatingStatsResponseDTO;
|
import com.lions.dev.dto.response.establishment.EstablishmentRatingStatsResponseDTO;
|
||||||
import com.lions.dev.entity.establishment.EstablishmentRating;
|
import com.lions.dev.entity.establishment.EstablishmentRating;
|
||||||
|
import com.lions.dev.security.Permission;
|
||||||
|
import com.lions.dev.security.RequiresPermission;
|
||||||
import com.lions.dev.service.EstablishmentRatingService;
|
import com.lions.dev.service.EstablishmentRatingService;
|
||||||
|
import com.lions.dev.service.SecurityService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
@@ -26,7 +28,10 @@ import java.util.UUID;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ressource REST pour la gestion des notations d'établissements.
|
* Ressource REST pour la gestion des notations d'établissements.
|
||||||
* Cette classe expose des endpoints pour soumettre, modifier et récupérer les notes.
|
*
|
||||||
|
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||||
*/
|
*/
|
||||||
@Path("/establishments/{establishmentId}/ratings")
|
@Path("/establishments/{establishmentId}/ratings")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -37,14 +42,10 @@ public class EstablishmentRatingResource {
|
|||||||
@Inject
|
@Inject
|
||||||
EstablishmentRatingService ratingService;
|
EstablishmentRatingService ratingService;
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(EstablishmentRatingResource.class);
|
@Inject
|
||||||
|
SecurityService securityService;
|
||||||
|
|
||||||
/**
|
private static final Logger LOG = Logger.getLogger(EstablishmentRatingResource.class);
|
||||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
|
||||||
*/
|
|
||||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
|
||||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soumet une nouvelle note pour un établissement.
|
* Soumet une nouvelle note pour un établissement.
|
||||||
@@ -52,7 +53,8 @@ public class EstablishmentRatingResource {
|
|||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.USER, UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.REVIEWS_CREATE)
|
||||||
@Operation(summary = "Soumettre une note pour un établissement",
|
@Operation(summary = "Soumettre une note pour un établissement",
|
||||||
description = "Soumet une nouvelle note (1 à 5 étoiles) pour un établissement. Requiert une authentification JWT.")
|
description = "Soumet une nouvelle note (1 à 5 étoiles) pour un établissement. Requiert une authentification JWT.")
|
||||||
@SecurityRequirement(name = "bearerAuth")
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
@@ -60,10 +62,9 @@ public class EstablishmentRatingResource {
|
|||||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||||
@APIResponse(responseCode = "400", description = "Données invalides")
|
@APIResponse(responseCode = "400", description = "Données invalides")
|
||||||
public Response submitRating(
|
public Response submitRating(
|
||||||
@Context ContainerRequestContext requestContext,
|
|
||||||
@PathParam("establishmentId") String establishmentId,
|
@PathParam("establishmentId") String establishmentId,
|
||||||
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
|
LOG.info("Soumission d'une note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -96,7 +97,8 @@ public class EstablishmentRatingResource {
|
|||||||
*/
|
*/
|
||||||
@PUT
|
@PUT
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.USER, UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.REVIEWS_UPDATE_OWN)
|
||||||
@Operation(summary = "Modifier une note existante",
|
@Operation(summary = "Modifier une note existante",
|
||||||
description = "Met à jour une note existante pour un établissement. Requiert une authentification JWT.")
|
description = "Met à jour une note existante pour un établissement. Requiert une authentification JWT.")
|
||||||
@SecurityRequirement(name = "bearerAuth")
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
@@ -104,10 +106,9 @@ public class EstablishmentRatingResource {
|
|||||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||||
@APIResponse(responseCode = "404", description = "Note non trouvée")
|
@APIResponse(responseCode = "404", description = "Note non trouvée")
|
||||||
public Response updateRating(
|
public Response updateRating(
|
||||||
@Context ContainerRequestContext requestContext,
|
|
||||||
@PathParam("establishmentId") String establishmentId,
|
@PathParam("establishmentId") String establishmentId,
|
||||||
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
@Valid EstablishmentRatingRequestDTO requestDTO) {
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
|
LOG.info("Mise à jour de la note pour l'établissement " + establishmentId + " par l'utilisateur " + authenticatedUserId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -136,10 +137,11 @@ public class EstablishmentRatingResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les statistiques de notation d'un établissement.
|
* Récupère les statistiques de notation d'un établissement.
|
||||||
* Doit être déclaré avant les endpoints génériques GET pour la résolution correcte par JAX-RS.
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/stats")
|
@Path("/stats")
|
||||||
|
@PermitAll
|
||||||
@Operation(summary = "Récupérer les statistiques de notation",
|
@Operation(summary = "Récupérer les statistiques de notation",
|
||||||
description = "Récupère les statistiques de notation d'un établissement (moyenne, total, distribution)")
|
description = "Récupère les statistiques de notation d'un établissement (moyenne, total, distribution)")
|
||||||
public Response getRatingStats(@PathParam("establishmentId") String establishmentId) {
|
public Response getRatingStats(@PathParam("establishmentId") String establishmentId) {
|
||||||
@@ -170,10 +172,11 @@ public class EstablishmentRatingResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère la note d'un utilisateur pour un établissement (via path parameter).
|
* Récupère la note d'un utilisateur pour un établissement (via path parameter).
|
||||||
* Endpoint alternatif pour compatibilité.
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/users/{userId}")
|
@Path("/users/{userId}")
|
||||||
|
@PermitAll
|
||||||
@Operation(summary = "Récupérer la note d'un utilisateur (path parameter)",
|
@Operation(summary = "Récupérer la note d'un utilisateur (path parameter)",
|
||||||
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via path parameter)")
|
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via path parameter)")
|
||||||
public Response getUserRatingByPath(
|
public Response getUserRatingByPath(
|
||||||
@@ -209,10 +212,10 @@ public class EstablishmentRatingResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère la note d'un utilisateur pour un établissement (via query parameter).
|
* Récupère la note d'un utilisateur pour un établissement (via query parameter).
|
||||||
* Endpoint utilisé par le frontend Flutter.
|
* Endpoint public.
|
||||||
* Doit être déclaré en dernier car c'est l'endpoint le plus générique.
|
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
@PermitAll
|
||||||
@Operation(summary = "Récupérer la note d'un utilisateur",
|
@Operation(summary = "Récupérer la note d'un utilisateur",
|
||||||
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via query parameter userId)")
|
description = "Récupère la note donnée par un utilisateur spécifique pour un établissement (via query parameter userId)")
|
||||||
public Response getUserRatingByQuery(
|
public Response getUserRatingByQuery(
|
||||||
|
|||||||
@@ -3,12 +3,16 @@ package com.lions.dev.resource;
|
|||||||
import com.lions.dev.dto.request.establishment.InitiateSubscriptionRequestDTO;
|
import com.lions.dev.dto.request.establishment.InitiateSubscriptionRequestDTO;
|
||||||
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
|
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
|
||||||
import com.lions.dev.service.WavePaymentService;
|
import com.lions.dev.service.WavePaymentService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
@@ -17,6 +21,10 @@ import java.util.UUID;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ressource pour les abonnements / droits d'accès des établissements (paiement Wave).
|
* Ressource pour les abonnements / droits d'accès des établissements (paiement Wave).
|
||||||
|
*
|
||||||
|
* SÉCURITÉ : L'initiation du paiement requiert une authentification, le statut est public.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||||
*/
|
*/
|
||||||
@Path("/establishments/{establishmentId}/subscriptions")
|
@Path("/establishments/{establishmentId}/subscriptions")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -32,11 +40,14 @@ public class EstablishmentSubscriptionResource {
|
|||||||
/**
|
/**
|
||||||
* Initie un paiement Wave pour les droits d'accès d'un établissement.
|
* Initie un paiement Wave pour les droits d'accès d'un établissement.
|
||||||
* Retourne l'URL de redirection vers la page de paiement Wave.
|
* Retourne l'URL de redirection vers la page de paiement Wave.
|
||||||
|
* Requiert une authentification (propriétaire ou manager de l'établissement).
|
||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Path("/initiate")
|
@Path("/initiate")
|
||||||
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
@Operation(summary = "Initier un paiement Wave (droits d'accès)",
|
@Operation(summary = "Initier un paiement Wave (droits d'accès)",
|
||||||
description = "Crée une session Wave et retourne l'URL de paiement. Le client redirige l'utilisateur vers payment_url.")
|
description = "Crée une session Wave et retourne l'URL de paiement. Le client redirige l'utilisateur vers payment_url.")
|
||||||
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
public Response initiatePayment(
|
public Response initiatePayment(
|
||||||
@PathParam("establishmentId") UUID establishmentId,
|
@PathParam("establishmentId") UUID establishmentId,
|
||||||
@Valid InitiateSubscriptionRequestDTO request) {
|
@Valid InitiateSubscriptionRequestDTO request) {
|
||||||
@@ -55,9 +66,11 @@ public class EstablishmentSubscriptionResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'établissement a un abonnement actif.
|
* Vérifie si l'établissement a un abonnement actif.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/status")
|
@Path("/status")
|
||||||
|
@PermitAll
|
||||||
@Operation(summary = "Statut d'abonnement", description = "Indique si l'établissement a des droits d'accès actifs.")
|
@Operation(summary = "Statut d'abonnement", description = "Indique si l'établissement a des droits d'accès actifs.")
|
||||||
public Response getSubscriptionStatus(@PathParam("establishmentId") UUID establishmentId) {
|
public Response getSubscriptionStatus(@PathParam("establishmentId") UUID establishmentId) {
|
||||||
boolean active = wavePaymentService.hasActiveSubscription(establishmentId);
|
boolean active = wavePaymentService.hasActiveSubscription(establishmentId);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.lions.dev.resource;
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
import com.lions.dev.service.FileService;
|
import com.lions.dev.service.FileService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
@@ -11,6 +14,9 @@ import jakarta.ws.rs.core.Context;
|
|||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.UriInfo;
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.reactive.RestForm;
|
import org.jboss.resteasy.reactive.RestForm;
|
||||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||||
@@ -22,7 +28,15 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ressource REST pour la gestion des uploads de fichiers (médias).
|
||||||
|
*
|
||||||
|
* SÉCURITÉ : L'upload requiert une authentification, la lecture est publique.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||||
|
*/
|
||||||
@Path("/media")
|
@Path("/media")
|
||||||
|
@Tag(name = "Media Upload", description = "Gestion des uploads de fichiers médias")
|
||||||
public class FileUploadResource {
|
public class FileUploadResource {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(FileUploadResource.class);
|
private static final Logger LOG = Logger.getLogger(FileUploadResource.class);
|
||||||
@@ -37,6 +51,9 @@ public class FileUploadResource {
|
|||||||
@Path("/upload")
|
@Path("/upload")
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@RolesAllowed({UserRoles.USER, UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@Operation(summary = "Uploader un fichier média", description = "Requiert une authentification JWT")
|
||||||
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
public Response uploadFile(
|
public Response uploadFile(
|
||||||
@RestForm("file") FileUpload file,
|
@RestForm("file") FileUpload file,
|
||||||
@RestForm("type") String type,
|
@RestForm("type") String type,
|
||||||
@@ -194,6 +211,8 @@ public class FileUploadResource {
|
|||||||
@GET
|
@GET
|
||||||
@Path("/files/{fileName}")
|
@Path("/files/{fileName}")
|
||||||
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
@Produces(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
@PermitAll
|
||||||
|
@Operation(summary = "Récupérer un fichier média", description = "Endpoint public pour servir les fichiers uploadés")
|
||||||
public Response getFile(@PathParam("fileName") String fileName) {
|
public Response getFile(@PathParam("fileName") String fileName) {
|
||||||
try {
|
try {
|
||||||
java.nio.file.Path filePath = java.nio.file.Paths.get("/tmp/uploads/", fileName);
|
java.nio.file.Path filePath = java.nio.file.Paths.get("/tmp/uploads/", fileName);
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
package com.lions.dev.resource;
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
import com.lions.dev.core.security.JwtAuthFilter;
|
|
||||||
import com.lions.dev.core.security.RequiresAuth;
|
|
||||||
import com.lions.dev.dto.request.promotion.PromotionCreateRequestDTO;
|
import com.lions.dev.dto.request.promotion.PromotionCreateRequestDTO;
|
||||||
import com.lions.dev.dto.request.promotion.PromotionUpdateRequestDTO;
|
import com.lions.dev.dto.request.promotion.PromotionUpdateRequestDTO;
|
||||||
import com.lions.dev.dto.response.promotion.PromotionResponseDTO;
|
import com.lions.dev.dto.response.promotion.PromotionResponseDTO;
|
||||||
import com.lions.dev.entity.promotion.Promotion;
|
import com.lions.dev.entity.promotion.Promotion;
|
||||||
|
import com.lions.dev.security.Permission;
|
||||||
|
import com.lions.dev.security.RequiresPermission;
|
||||||
import com.lions.dev.service.PromotionService;
|
import com.lions.dev.service.PromotionService;
|
||||||
|
import com.lions.dev.service.SecurityService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
@@ -29,8 +31,10 @@ import java.util.stream.Collectors;
|
|||||||
/**
|
/**
|
||||||
* Ressource REST pour la gestion des promotions dans le système AfterWork.
|
* Ressource REST pour la gestion des promotions dans le système AfterWork.
|
||||||
*
|
*
|
||||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour
|
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||||
* et supprimer des promotions d'établissements.
|
* Seul le responsable de l'établissement peut créer/modifier/supprimer des promotions.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||||
*/
|
*/
|
||||||
@Path("/promotions")
|
@Path("/promotions")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -41,14 +45,10 @@ public class PromotionResource {
|
|||||||
@Inject
|
@Inject
|
||||||
PromotionService promotionService;
|
PromotionService promotionService;
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(PromotionResource.class);
|
@Inject
|
||||||
|
SecurityService securityService;
|
||||||
|
|
||||||
/**
|
private static final Logger LOG = Logger.getLogger(PromotionResource.class);
|
||||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
|
||||||
*/
|
|
||||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
|
||||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// ENDPOINTS PUBLICS (LECTURE)
|
// ENDPOINTS PUBLICS (LECTURE)
|
||||||
@@ -62,6 +62,7 @@ public class PromotionResource {
|
|||||||
* @return Liste paginée des promotions actives
|
* @return Liste paginée des promotions actives
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer toutes les promotions actives",
|
summary = "Récupérer toutes les promotions actives",
|
||||||
description = "Retourne une liste paginée de toutes les promotions actives et valides")
|
description = "Retourne une liste paginée de toutes les promotions actives et valides")
|
||||||
@@ -94,6 +95,7 @@ public class PromotionResource {
|
|||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer une promotion par ID",
|
summary = "Récupérer une promotion par ID",
|
||||||
description = "Retourne les détails d'une promotion spécifique")
|
description = "Retourne les détails d'une promotion spécifique")
|
||||||
@@ -127,6 +129,7 @@ public class PromotionResource {
|
|||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/code/{code}")
|
@Path("/code/{code}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Rechercher une promotion par code promo",
|
summary = "Rechercher une promotion par code promo",
|
||||||
description = "Retourne la promotion correspondant au code promo")
|
description = "Retourne la promotion correspondant au code promo")
|
||||||
@@ -164,6 +167,7 @@ public class PromotionResource {
|
|||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/establishment/{establishmentId}")
|
@Path("/establishment/{establishmentId}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer les promotions d'un établissement",
|
summary = "Récupérer les promotions d'un établissement",
|
||||||
description = "Retourne les promotions d'un établissement spécifique")
|
description = "Retourne les promotions d'un établissement spécifique")
|
||||||
@@ -211,7 +215,8 @@ public class PromotionResource {
|
|||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.PROMOTIONS_CREATE)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Créer une promotion",
|
summary = "Créer une promotion",
|
||||||
description = "Crée une nouvelle promotion pour un établissement. Seul le responsable peut créer.")
|
description = "Crée une nouvelle promotion pour un établissement. Seul le responsable peut créer.")
|
||||||
@@ -220,10 +225,8 @@ public class PromotionResource {
|
|||||||
@APIResponse(responseCode = "400", description = "Données invalides")
|
@APIResponse(responseCode = "400", description = "Données invalides")
|
||||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||||
@APIResponse(responseCode = "403", description = "Non autorisé à créer des promotions pour cet établissement")
|
@APIResponse(responseCode = "403", description = "Non autorisé à créer des promotions pour cet établissement")
|
||||||
public Response createPromotion(
|
public Response createPromotion(@Valid PromotionCreateRequestDTO requestDTO) {
|
||||||
@Context ContainerRequestContext requestContext,
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
@Valid PromotionCreateRequestDTO requestDTO) {
|
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
|
||||||
LOG.info("[LOG] Création d'une promotion pour l'établissement : " + requestDTO.getEstablishmentId() +
|
LOG.info("[LOG] Création d'une promotion pour l'établissement : " + requestDTO.getEstablishmentId() +
|
||||||
" par l'utilisateur : " + authenticatedUserId);
|
" par l'utilisateur : " + authenticatedUserId);
|
||||||
|
|
||||||
@@ -267,7 +270,8 @@ public class PromotionResource {
|
|||||||
@PUT
|
@PUT
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.PROMOTIONS_UPDATE_OWN)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Mettre à jour une promotion",
|
summary = "Mettre à jour une promotion",
|
||||||
description = "Met à jour une promotion existante. Seul le responsable peut modifier.")
|
description = "Met à jour une promotion existante. Seul le responsable peut modifier.")
|
||||||
@@ -277,10 +281,9 @@ public class PromotionResource {
|
|||||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cette promotion")
|
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cette promotion")
|
||||||
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
||||||
public Response updatePromotion(
|
public Response updatePromotion(
|
||||||
@Context ContainerRequestContext requestContext,
|
|
||||||
@PathParam("id") UUID promotionId,
|
@PathParam("id") UUID promotionId,
|
||||||
@Valid PromotionUpdateRequestDTO requestDTO) {
|
@Valid PromotionUpdateRequestDTO requestDTO) {
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
LOG.info("[LOG] Mise à jour de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
|
LOG.info("[LOG] Mise à jour de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -321,7 +324,8 @@ public class PromotionResource {
|
|||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.PROMOTIONS_DELETE_OWN)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Supprimer une promotion",
|
summary = "Supprimer une promotion",
|
||||||
description = "Supprime une promotion. Seul le responsable peut supprimer.")
|
description = "Supprime une promotion. Seul le responsable peut supprimer.")
|
||||||
@@ -330,10 +334,8 @@ public class PromotionResource {
|
|||||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cette promotion")
|
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cette promotion")
|
||||||
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
||||||
public Response deletePromotion(
|
public Response deletePromotion(@PathParam("id") UUID promotionId) {
|
||||||
@Context ContainerRequestContext requestContext,
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
@PathParam("id") UUID promotionId) {
|
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
|
||||||
LOG.info("[LOG] Suppression de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
|
LOG.info("[LOG] Suppression de la promotion : " + promotionId + " par l'utilisateur : " + authenticatedUserId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -379,7 +381,8 @@ public class PromotionResource {
|
|||||||
@PATCH
|
@PATCH
|
||||||
@Path("/{id}/active")
|
@Path("/{id}/active")
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.OWNER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.PROMOTIONS_UPDATE_OWN)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Activer/Désactiver une promotion",
|
summary = "Activer/Désactiver une promotion",
|
||||||
description = "Change l'état actif d'une promotion. Seul le responsable peut modifier.")
|
description = "Change l'état actif d'une promotion. Seul le responsable peut modifier.")
|
||||||
@@ -389,10 +392,9 @@ public class PromotionResource {
|
|||||||
@APIResponse(responseCode = "403", description = "Non autorisé")
|
@APIResponse(responseCode = "403", description = "Non autorisé")
|
||||||
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
@APIResponse(responseCode = "404", description = "Promotion non trouvée")
|
||||||
public Response setPromotionActive(
|
public Response setPromotionActive(
|
||||||
@Context ContainerRequestContext requestContext,
|
|
||||||
@PathParam("id") UUID promotionId,
|
@PathParam("id") UUID promotionId,
|
||||||
@QueryParam("active") @DefaultValue("true") boolean isActive) {
|
@QueryParam("active") @DefaultValue("true") boolean isActive) {
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
LOG.info("[LOG] Changement d'état de la promotion " + promotionId + " à " + isActive);
|
LOG.info("[LOG] Changement d'état de la promotion " + promotionId + " à " + isActive);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
package com.lions.dev.resource;
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
import com.lions.dev.core.security.JwtAuthFilter;
|
|
||||||
import com.lions.dev.core.security.RequiresAuth;
|
|
||||||
import com.lions.dev.dto.request.review.ReviewCreateRequestDTO;
|
import com.lions.dev.dto.request.review.ReviewCreateRequestDTO;
|
||||||
import com.lions.dev.dto.request.review.ReviewUpdateRequestDTO;
|
import com.lions.dev.dto.request.review.ReviewUpdateRequestDTO;
|
||||||
import com.lions.dev.dto.response.review.ReviewResponseDTO;
|
import com.lions.dev.dto.response.review.ReviewResponseDTO;
|
||||||
import com.lions.dev.entity.establishment.Review;
|
import com.lions.dev.entity.establishment.Review;
|
||||||
|
import com.lions.dev.security.Permission;
|
||||||
|
import com.lions.dev.security.RequiresPermission;
|
||||||
import com.lions.dev.service.ReviewService;
|
import com.lions.dev.service.ReviewService;
|
||||||
|
import com.lions.dev.service.SecurityService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.ws.rs.*;
|
import jakarta.ws.rs.*;
|
||||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
@@ -30,8 +32,10 @@ import java.util.stream.Collectors;
|
|||||||
/**
|
/**
|
||||||
* Ressource REST pour la gestion des avis d'établissements.
|
* Ressource REST pour la gestion des avis d'établissements.
|
||||||
*
|
*
|
||||||
* Cette classe expose des endpoints pour créer, récupérer, mettre à jour
|
* SÉCURITÉ : Les lectures sont publiques, les écritures requièrent une authentification.
|
||||||
* et supprimer des avis sur les établissements.
|
* L'utilisateur ne peut modifier/supprimer que SES PROPRES avis.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Sécurité JWT + RBAC production-ready
|
||||||
*/
|
*/
|
||||||
@Path("/reviews")
|
@Path("/reviews")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -42,14 +46,10 @@ public class ReviewResource {
|
|||||||
@Inject
|
@Inject
|
||||||
ReviewService reviewService;
|
ReviewService reviewService;
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(ReviewResource.class);
|
@Inject
|
||||||
|
SecurityService securityService;
|
||||||
|
|
||||||
/**
|
private static final Logger LOG = Logger.getLogger(ReviewResource.class);
|
||||||
* Extrait l'ID de l'utilisateur authentifié du contexte de la requête.
|
|
||||||
*/
|
|
||||||
private UUID getAuthenticatedUserId(ContainerRequestContext requestContext) {
|
|
||||||
return (UUID) requestContext.getProperty(JwtAuthFilter.AUTHENTICATED_USER_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// ENDPOINTS PUBLICS (LECTURE)
|
// ENDPOINTS PUBLICS (LECTURE)
|
||||||
@@ -57,9 +57,11 @@ public class ReviewResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère un avis par son ID.
|
* Récupère un avis par son ID.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer un avis par ID",
|
summary = "Récupérer un avis par ID",
|
||||||
description = "Retourne les détails d'un avis spécifique")
|
description = "Retourne les détails d'un avis spécifique")
|
||||||
@@ -87,9 +89,11 @@ public class ReviewResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les avis d'un établissement.
|
* Récupère les avis d'un établissement.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/establishment/{establishmentId}")
|
@Path("/establishment/{establishmentId}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer les avis d'un établissement",
|
summary = "Récupérer les avis d'un établissement",
|
||||||
description = "Retourne la liste paginée des avis pour un établissement")
|
description = "Retourne la liste paginée des avis pour un établissement")
|
||||||
@@ -124,9 +128,11 @@ public class ReviewResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les statistiques des avis pour un établissement.
|
* Récupère les statistiques des avis pour un établissement.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/establishment/{establishmentId}/stats")
|
@Path("/establishment/{establishmentId}/stats")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer les statistiques des avis",
|
summary = "Récupérer les statistiques des avis",
|
||||||
description = "Retourne les statistiques (moyenne, distribution, etc.) des avis pour un établissement")
|
description = "Retourne les statistiques (moyenne, distribution, etc.) des avis pour un établissement")
|
||||||
@@ -147,9 +153,11 @@ public class ReviewResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les avis d'un utilisateur.
|
* Récupère les avis d'un utilisateur.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/user/{userId}")
|
@Path("/user/{userId}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer les avis d'un utilisateur",
|
summary = "Récupérer les avis d'un utilisateur",
|
||||||
description = "Retourne la liste paginée des avis écrits par un utilisateur")
|
description = "Retourne la liste paginée des avis écrits par un utilisateur")
|
||||||
@@ -177,9 +185,11 @@ public class ReviewResource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'utilisateur a déjà écrit un avis pour un établissement.
|
* Vérifie si l'utilisateur a déjà écrit un avis pour un établissement.
|
||||||
|
* Endpoint public.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("/establishment/{establishmentId}/user/{userId}")
|
@Path("/establishment/{establishmentId}/user/{userId}")
|
||||||
|
@PermitAll
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Récupérer l'avis d'un utilisateur pour un établissement",
|
summary = "Récupérer l'avis d'un utilisateur pour un établissement",
|
||||||
description = "Retourne l'avis si l'utilisateur en a écrit un, 404 sinon")
|
description = "Retourne l'avis si l'utilisateur en a écrit un, 404 sinon")
|
||||||
@@ -218,7 +228,8 @@ public class ReviewResource {
|
|||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.REVIEWS_CREATE)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Créer un avis",
|
summary = "Créer un avis",
|
||||||
description = "Crée un nouvel avis pour un établissement. Un seul avis par utilisateur et établissement.")
|
description = "Crée un nouvel avis pour un établissement. Un seul avis par utilisateur et établissement.")
|
||||||
@@ -226,10 +237,8 @@ public class ReviewResource {
|
|||||||
@APIResponse(responseCode = "201", description = "Avis créé avec succès")
|
@APIResponse(responseCode = "201", description = "Avis créé avec succès")
|
||||||
@APIResponse(responseCode = "400", description = "Données invalides ou avis déjà existant")
|
@APIResponse(responseCode = "400", description = "Données invalides ou avis déjà existant")
|
||||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||||
public Response createReview(
|
public Response createReview(@Valid ReviewCreateRequestDTO requestDTO) {
|
||||||
@Context ContainerRequestContext requestContext,
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
@Valid ReviewCreateRequestDTO requestDTO) {
|
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
|
||||||
LOG.info("[LOG] Création d'un avis pour l'établissement : " + requestDTO.getEstablishmentId() +
|
LOG.info("[LOG] Création d'un avis pour l'établissement : " + requestDTO.getEstablishmentId() +
|
||||||
" par l'utilisateur : " + authenticatedUserId);
|
" par l'utilisateur : " + authenticatedUserId);
|
||||||
|
|
||||||
@@ -258,7 +267,8 @@ public class ReviewResource {
|
|||||||
@PUT
|
@PUT
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.REVIEWS_UPDATE_OWN)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Mettre à jour un avis",
|
summary = "Mettre à jour un avis",
|
||||||
description = "Met à jour un avis existant. Seul l'auteur peut modifier.")
|
description = "Met à jour un avis existant. Seul l'auteur peut modifier.")
|
||||||
@@ -268,10 +278,9 @@ public class ReviewResource {
|
|||||||
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet avis")
|
@APIResponse(responseCode = "403", description = "Non autorisé à modifier cet avis")
|
||||||
@APIResponse(responseCode = "404", description = "Avis non trouvé")
|
@APIResponse(responseCode = "404", description = "Avis non trouvé")
|
||||||
public Response updateReview(
|
public Response updateReview(
|
||||||
@Context ContainerRequestContext requestContext,
|
|
||||||
@PathParam("id") UUID reviewId,
|
@PathParam("id") UUID reviewId,
|
||||||
@Valid ReviewUpdateRequestDTO requestDTO) {
|
@Valid ReviewUpdateRequestDTO requestDTO) {
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
LOG.info("[LOG] Mise à jour de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
|
LOG.info("[LOG] Mise à jour de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -304,7 +313,8 @@ public class ReviewResource {
|
|||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
@Transactional
|
@Transactional
|
||||||
@RequiresAuth
|
@RolesAllowed({UserRoles.USER, UserRoles.MANAGER, UserRoles.ADMIN, UserRoles.SUPER_ADMIN})
|
||||||
|
@RequiresPermission(Permission.REVIEWS_DELETE_OWN)
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Supprimer un avis",
|
summary = "Supprimer un avis",
|
||||||
description = "Supprime un avis. Seul l'auteur peut supprimer.")
|
description = "Supprime un avis. Seul l'auteur peut supprimer.")
|
||||||
@@ -313,10 +323,8 @@ public class ReviewResource {
|
|||||||
@APIResponse(responseCode = "401", description = "Non authentifié")
|
@APIResponse(responseCode = "401", description = "Non authentifié")
|
||||||
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cet avis")
|
@APIResponse(responseCode = "403", description = "Non autorisé à supprimer cet avis")
|
||||||
@APIResponse(responseCode = "404", description = "Avis non trouvé")
|
@APIResponse(responseCode = "404", description = "Avis non trouvé")
|
||||||
public Response deleteReview(
|
public Response deleteReview(@PathParam("id") UUID reviewId) {
|
||||||
@Context ContainerRequestContext requestContext,
|
UUID authenticatedUserId = securityService.getCurrentUserId();
|
||||||
@PathParam("id") UUID reviewId) {
|
|
||||||
UUID authenticatedUserId = getAuthenticatedUserId(requestContext);
|
|
||||||
LOG.info("[LOG] Suppression de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
|
LOG.info("[LOG] Suppression de l'avis : " + reviewId + " par l'utilisateur : " + authenticatedUserId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# AfterWork Server - Configuration PRODUCTION (profil prod)
|
# AfterWork Server - Configuration PRODUCTION (profil prod)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Charg? avec QUARKUS_PROFILE=prod (Kubernetes ConfigMap).
|
# Chargé avec QUARKUS_PROFILE=prod (Kubernetes ConfigMap).
|
||||||
# Ce fichier remplace application-production.properties pour coh?rence
|
#
|
||||||
# avec le d?ploiement (QUARKUS_PROFILE=prod).
|
# INFRASTRUCTURE LIONS:
|
||||||
|
# - Prometheus: https://prometheus.lions.dev (scraping automatique via annotations)
|
||||||
|
# - Grafana: https://grafana.lions.dev (dashboard AfterWork disponible)
|
||||||
|
# - Vault: https://vault.lions.dev (gestion secrets si déverrouillé)
|
||||||
|
# - Kafka: kafka-service.kafka.svc.cluster.local:9092
|
||||||
|
# - PostgreSQL: postgresql-service.postgresql.svc.cluster.local:5432
|
||||||
|
# - Keycloak: https://security.lions.dev (SSO optionnel)
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# HTTP - Chemin de base de l'API
|
# HTTP - Chemin de base de l'API
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Permet d'acc?der ? l'API via https://api.lions.dev/afterwork
|
# Accessible via https://api.lions.dev/afterwork
|
||||||
#
|
#
|
||||||
# IMPORTANT - Configuration Ingress requise:
|
# IMPORTANT - Configuration Ingress:
|
||||||
# Cette application utilise quarkus.http.root-path pour ?tre "context-aware",
|
# L'application est "context-aware" (quarkus.http.root-path=/afterwork).
|
||||||
# ce qui permet ? Swagger UI de g?n?rer les bonnes URLs.
|
# L'Ingress Kubernetes doit préserver le chemin complet (PAS de rewrite-target).
|
||||||
# L'Ingress Kubernetes DOIT pr?server le chemin complet (PAS de rewrite-target).
|
|
||||||
#
|
|
||||||
# Configuration Ingress correcte:
|
|
||||||
# - path: /afterwork
|
|
||||||
# - pathType: Prefix
|
|
||||||
# - PAS d'annotation rewrite-target
|
|
||||||
#
|
#
|
||||||
quarkus.http.root-path=/afterwork
|
quarkus.http.root-path=/afterwork
|
||||||
|
|
||||||
|
|||||||
163
src/test/java/com/lions/dev/resource/BookingResourceTest.java
Normal file
163
src/test/java/com/lions/dev/resource/BookingResourceTest.java
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour BookingResource.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Tests production-ready
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class BookingResourceTest {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests d'accès sans authentification
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /bookings - Sans authentification devrait retourner 401")
|
||||||
|
void testGetAllBookingsWithoutAuth() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/bookings")
|
||||||
|
.then()
|
||||||
|
.statusCode(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /bookings - Sans authentification devrait retourner 401")
|
||||||
|
void testCreateBookingWithoutAuth() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"eventId": "00000000-0000-0000-0000-000000000001",
|
||||||
|
"userId": "00000000-0000-0000-0000-000000000002",
|
||||||
|
"seats": 2
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/bookings")
|
||||||
|
.then()
|
||||||
|
.statusCode(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests avec authentification USER
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("GET /bookings/user/{userId} - Accès aux propres réservations")
|
||||||
|
void testGetOwnBookings() {
|
||||||
|
UUID userId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/bookings/user/" + userId)
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(403))); // 403 si userId ne correspond pas
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("GET /bookings/{id} - Réservation inexistante devrait retourner 404")
|
||||||
|
void testGetNonExistentBooking() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/bookings/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("DELETE /bookings/{id} - Réservation inexistante devrait retourner 404")
|
||||||
|
void testCancelNonExistentBooking() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.delete("/afterwork/bookings/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests avec authentification ADMIN
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("GET /bookings - ADMIN peut voir toutes les réservations")
|
||||||
|
void testAdminCanViewAllBookings() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/bookings")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$", instanceOf(java.util.List.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("GET /bookings/event/{eventId} - ADMIN peut voir les réservations d'un événement")
|
||||||
|
void testAdminCanViewEventBookings() {
|
||||||
|
UUID eventId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/bookings/event/" + eventId)
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests avec authentification OWNER/MANAGER
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "owner@test.com", roles = {"OWNER"})
|
||||||
|
@DisplayName("GET /bookings/establishment/{id} - OWNER peut voir les réservations de son établissement")
|
||||||
|
void testOwnerCanViewEstablishmentBookings() {
|
||||||
|
UUID establishmentId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/bookings/establishment/" + establishmentId)
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404), is(403)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests de validation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("POST /bookings - Données invalides devrait retourner 400")
|
||||||
|
void testCreateBookingWithInvalidData() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"seats": -1
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/bookings")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour EstablishmentResource.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Tests production-ready
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class EstablishmentResourceTest {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests endpoints publics (lecture)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /establishments - Liste des établissements (public)")
|
||||||
|
void testGetAllEstablishments() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/establishments")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$", instanceOf(java.util.List.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /establishments/{id} - Établissement inexistant devrait retourner 404")
|
||||||
|
void testGetNonExistentEstablishment() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/establishments/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /establishments/search - Recherche d'établissements")
|
||||||
|
void testSearchEstablishments() {
|
||||||
|
given()
|
||||||
|
.queryParam("q", "bar")
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/establishments/search")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$", instanceOf(java.util.List.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /establishments/nearby - Recherche par proximité")
|
||||||
|
void testGetNearbyEstablishments() {
|
||||||
|
given()
|
||||||
|
.queryParam("lat", 5.3600)
|
||||||
|
.queryParam("lng", -3.9400)
|
||||||
|
.queryParam("radius", 10)
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/establishments/nearby")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$", instanceOf(java.util.List.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests endpoints protégés (création, modification)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /establishments - Sans authentification devrait retourner 401")
|
||||||
|
void testCreateEstablishmentWithoutAuth() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"name": "Test Bar",
|
||||||
|
"description": "Un bar de test",
|
||||||
|
"address": "123 Rue Test",
|
||||||
|
"city": "Abidjan"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/establishments")
|
||||||
|
.then()
|
||||||
|
.statusCode(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("POST /establishments - USER ne peut pas créer d'établissement")
|
||||||
|
void testCreateEstablishmentWithUserRole() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"name": "Test Bar",
|
||||||
|
"description": "Un bar de test",
|
||||||
|
"address": "123 Rue Test",
|
||||||
|
"city": "Abidjan"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/establishments")
|
||||||
|
.then()
|
||||||
|
.statusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "owner@test.com", roles = {"OWNER"})
|
||||||
|
@DisplayName("POST /establishments - OWNER peut créer un établissement")
|
||||||
|
void testCreateEstablishmentWithOwnerRole() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"name": "Test Bar",
|
||||||
|
"description": "Un bar de test",
|
||||||
|
"address": "123 Rue Test",
|
||||||
|
"city": "Abidjan",
|
||||||
|
"phone": "+22501234567",
|
||||||
|
"email": "testbar@test.com",
|
||||||
|
"latitude": 5.3600,
|
||||||
|
"longitude": -3.9400
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/establishments")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(201), is(200), is(400))); // 400 si validation échoue
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "owner@test.com", roles = {"OWNER"})
|
||||||
|
@DisplayName("PUT /establishments/{id} - OWNER peut modifier un établissement")
|
||||||
|
void testUpdateEstablishmentWithOwnerRole() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"name": "Updated Bar Name",
|
||||||
|
"description": "Description mise à jour"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.put("/afterwork/establishments/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404), is(403))); // 404 si n'existe pas, 403 si pas propriétaire
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "owner@test.com", roles = {"OWNER"})
|
||||||
|
@DisplayName("DELETE /establishments/{id} - Établissement inexistant devrait retourner 404")
|
||||||
|
void testDeleteNonExistentEstablishment() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.delete("/afterwork/establishments/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests endpoints admin
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("GET /establishments/pending - ADMIN peut voir les établissements en attente")
|
||||||
|
void testGetPendingEstablishments() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/establishments/pending")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("PATCH /establishments/{id}/approve - ADMIN peut approuver un établissement")
|
||||||
|
void testApproveEstablishment() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.patch("/afterwork/establishments/" + randomId + "/approve")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests de validation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "owner@test.com", roles = {"OWNER"})
|
||||||
|
@DisplayName("POST /establishments - Données invalides devrait retourner 400")
|
||||||
|
void testCreateEstablishmentWithInvalidData() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"name": ""
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/establishments")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/test/java/com/lions/dev/resource/UsersResourceTest.java
Normal file
215
src/test/java/com/lions/dev/resource/UsersResourceTest.java
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour UsersResource.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Tests production-ready
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class UsersResourceTest {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests endpoints publics (authentification, inscription)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /users/authenticate - Identifiants invalides devrait retourner 401")
|
||||||
|
void testAuthenticateWithInvalidCredentials() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"email": "invalid@test.com",
|
||||||
|
"password": "wrongpassword"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/users/authenticate")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(401), is(404)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /users/authenticate - Email manquant devrait retourner 400")
|
||||||
|
void testAuthenticateWithMissingEmail() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"password": "somepassword"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/users/authenticate")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /users/register - Email invalide devrait retourner 400")
|
||||||
|
void testRegisterWithInvalidEmail() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"email": "invalidemail",
|
||||||
|
"password": "ValidPassword123!",
|
||||||
|
"firstName": "Test",
|
||||||
|
"lastName": "User"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/users/register")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /users/forgot-password - Email inexistant devrait retourner 404 ou 200 (sécurité)")
|
||||||
|
void testForgotPasswordWithNonExistentEmail() {
|
||||||
|
// Pour des raisons de sécurité, certaines implémentations retournent 200
|
||||||
|
// même si l'email n'existe pas (pour ne pas divulguer d'informations)
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"email": "nonexistent@test.com"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/users/forgot-password")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests endpoints protégés (profil utilisateur)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /users/me - Sans authentification devrait retourner 401")
|
||||||
|
void testGetProfileWithoutAuth() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/users/me")
|
||||||
|
.then()
|
||||||
|
.statusCode(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "testuser@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("GET /users/me - Avec authentification devrait retourner le profil")
|
||||||
|
void testGetProfileWithAuth() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/users/me")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(200), is(404))); // 404 si l'utilisateur de test n'existe pas en BDD
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("PUT /users/{id} - Sans authentification devrait retourner 401")
|
||||||
|
void testUpdateProfileWithoutAuth() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"firstName": "Updated",
|
||||||
|
"lastName": "User"
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
.when()
|
||||||
|
.put("/afterwork/users/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests endpoints admin
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("GET /users - Sans authentification devrait retourner 401")
|
||||||
|
void testGetAllUsersWithoutAuth() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("GET /users - Avec rôle USER devrait retourner 403")
|
||||||
|
void testGetAllUsersWithUserRole() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("GET /users - Avec rôle ADMIN devrait retourner 200")
|
||||||
|
void testGetAllUsersWithAdminRole() {
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.get("/afterwork/users")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("$", instanceOf(java.util.List.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("DELETE /users/{id} - Suppression d'un utilisateur inexistant devrait retourner 404")
|
||||||
|
void testDeleteNonExistentUser() {
|
||||||
|
UUID randomId = UUID.randomUUID();
|
||||||
|
|
||||||
|
given()
|
||||||
|
.when()
|
||||||
|
.delete("/afterwork/users/" + randomId)
|
||||||
|
.then()
|
||||||
|
.statusCode(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests de validation
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("POST /users/register - Données complètes valides")
|
||||||
|
void testRegisterValidation() {
|
||||||
|
String uniqueEmail = "test_" + System.currentTimeMillis() + "@test.com";
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(String.format("""
|
||||||
|
{
|
||||||
|
"email": "%s",
|
||||||
|
"password": "ValidPassword123!",
|
||||||
|
"firstName": "Test",
|
||||||
|
"lastName": "User",
|
||||||
|
"phone": "+33612345678"
|
||||||
|
}
|
||||||
|
""", uniqueEmail))
|
||||||
|
.when()
|
||||||
|
.post("/afterwork/users/register")
|
||||||
|
.then()
|
||||||
|
.statusCode(anyOf(is(201), is(200), is(409))); // 409 si l'email existe déjà
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/test/java/com/lions/dev/service/MessageServiceTest.java
Normal file
113
src/test/java/com/lions/dev/service/MessageServiceTest.java
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package com.lions.dev.service;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour MessageService.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Tests production-ready
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class MessageServiceTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MessageService messageService;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests de base
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Service devrait être injecté")
|
||||||
|
void testServiceInjection() {
|
||||||
|
assertNotNull(messageService, "MessageService devrait être injecté");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getUserConversations devrait retourner une liste vide pour un utilisateur sans conversations")
|
||||||
|
void testGetUserConversationsEmpty() {
|
||||||
|
UUID randomUserId = UUID.randomUUID();
|
||||||
|
|
||||||
|
var conversations = messageService.getUserConversations(randomUserId);
|
||||||
|
|
||||||
|
assertNotNull(conversations, "La liste ne devrait pas être null");
|
||||||
|
assertTrue(conversations.isEmpty(), "La liste devrait être vide pour un nouvel utilisateur");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getConversation devrait retourner null pour une conversation inexistante")
|
||||||
|
void testGetNonExistentConversation() {
|
||||||
|
UUID randomConvId = UUID.randomUUID();
|
||||||
|
|
||||||
|
var conversation = messageService.getConversation(randomConvId);
|
||||||
|
|
||||||
|
assertNull(conversation, "Devrait retourner null pour une conversation inexistante");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getConversationMessages devrait retourner une liste vide pour une conversation inexistante")
|
||||||
|
void testGetMessagesForNonExistentConversation() {
|
||||||
|
UUID randomConvId = UUID.randomUUID();
|
||||||
|
|
||||||
|
var messages = messageService.getConversationMessages(randomConvId, 0, 50);
|
||||||
|
|
||||||
|
assertNotNull(messages, "La liste ne devrait pas être null");
|
||||||
|
assertTrue(messages.isEmpty(), "La liste devrait être vide");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getTotalUnreadCount devrait retourner 0 pour un utilisateur sans messages")
|
||||||
|
void testGetUnreadCountForNewUser() {
|
||||||
|
UUID randomUserId = UUID.randomUUID();
|
||||||
|
|
||||||
|
long count = messageService.getTotalUnreadCount(randomUserId);
|
||||||
|
|
||||||
|
assertEquals(0, count, "Le compte de messages non lus devrait être 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("deleteMessage devrait retourner false pour un message inexistant")
|
||||||
|
void testDeleteNonExistentMessage() {
|
||||||
|
UUID randomMessageId = UUID.randomUUID();
|
||||||
|
|
||||||
|
boolean deleted = messageService.deleteMessage(randomMessageId);
|
||||||
|
|
||||||
|
assertFalse(deleted, "Devrait retourner false pour un message inexistant");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("deleteConversation devrait retourner false pour une conversation inexistante")
|
||||||
|
void testDeleteNonExistentConversation() {
|
||||||
|
UUID randomConvId = UUID.randomUUID();
|
||||||
|
|
||||||
|
boolean deleted = messageService.deleteConversation(randomConvId);
|
||||||
|
|
||||||
|
assertFalse(deleted, "Devrait retourner false pour une conversation inexistante");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests de pagination
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getConversationMessages devrait respecter les paramètres de pagination")
|
||||||
|
void testMessagesPagination() {
|
||||||
|
UUID randomConvId = UUID.randomUUID();
|
||||||
|
|
||||||
|
// Page 0, size 10
|
||||||
|
var page1 = messageService.getConversationMessages(randomConvId, 0, 10);
|
||||||
|
assertNotNull(page1);
|
||||||
|
|
||||||
|
// Page 1, size 10
|
||||||
|
var page2 = messageService.getConversationMessages(randomConvId, 1, 10);
|
||||||
|
assertNotNull(page2);
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/test/java/com/lions/dev/service/SecurityServiceTest.java
Normal file
164
src/test/java/com/lions/dev/service/SecurityServiceTest.java
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package com.lions.dev.service;
|
||||||
|
|
||||||
|
import com.lions.dev.security.Permission;
|
||||||
|
import com.lions.dev.security.Role;
|
||||||
|
import com.lions.dev.security.RolePermissionConfig;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour le SecurityService.
|
||||||
|
* Utilise @TestSecurity pour simuler différents contextes de sécurité.
|
||||||
|
*
|
||||||
|
* @since 2.0 - Tests production-ready
|
||||||
|
*/
|
||||||
|
@QuarkusTest
|
||||||
|
class SecurityServiceTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SecurityService securityService;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests des permissions par rôle
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Role USER devrait avoir les permissions de base")
|
||||||
|
void testUserRoleHasBasicPermissions() {
|
||||||
|
Set<Permission> userPermissions = RolePermissionConfig.getPermissions(Role.USER);
|
||||||
|
|
||||||
|
// Vérifier les permissions de profil
|
||||||
|
assertTrue(userPermissions.contains(Permission.PROFILE_READ), "USER devrait pouvoir lire son profil");
|
||||||
|
assertTrue(userPermissions.contains(Permission.PROFILE_UPDATE), "USER devrait pouvoir modifier son profil");
|
||||||
|
|
||||||
|
// Vérifier les permissions de lecture
|
||||||
|
assertTrue(userPermissions.contains(Permission.EVENTS_READ), "USER devrait pouvoir lire les événements");
|
||||||
|
assertTrue(userPermissions.contains(Permission.ESTABLISHMENTS_READ), "USER devrait pouvoir lire les établissements");
|
||||||
|
|
||||||
|
// Vérifier les permissions de participation
|
||||||
|
assertTrue(userPermissions.contains(Permission.EVENTS_PARTICIPATE), "USER devrait pouvoir participer aux événements");
|
||||||
|
assertTrue(userPermissions.contains(Permission.RESERVATIONS_CREATE), "USER devrait pouvoir créer des réservations");
|
||||||
|
|
||||||
|
// Vérifier l'absence de permissions admin
|
||||||
|
assertFalse(userPermissions.contains(Permission.SUPER_ADMIN_ACCESS), "USER ne devrait pas avoir accès super admin");
|
||||||
|
assertFalse(userPermissions.contains(Permission.USERS_DELETE_ANY), "USER ne devrait pas pouvoir supprimer d'autres utilisateurs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Role OWNER devrait avoir les permissions d'établissement")
|
||||||
|
void testOwnerRoleHasEstablishmentPermissions() {
|
||||||
|
Set<Permission> ownerPermissions = RolePermissionConfig.getPermissions(Role.OWNER);
|
||||||
|
|
||||||
|
// Permissions de gestion d'établissement
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.ESTABLISHMENTS_CREATE), "OWNER devrait pouvoir créer un établissement");
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.ESTABLISHMENTS_UPDATE_OWN), "OWNER devrait pouvoir modifier son établissement");
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.ESTABLISHMENTS_DELETE_OWN), "OWNER devrait pouvoir supprimer son établissement");
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.ESTABLISHMENTS_MANAGE_STAFF), "OWNER devrait pouvoir gérer le personnel");
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.ESTABLISHMENTS_VIEW_ANALYTICS), "OWNER devrait voir les analytics");
|
||||||
|
|
||||||
|
// Permissions de promotions
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.PROMOTIONS_CREATE), "OWNER devrait pouvoir créer des promotions");
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.PROMOTIONS_UPDATE_OWN), "OWNER devrait pouvoir modifier ses promotions");
|
||||||
|
assertTrue(ownerPermissions.contains(Permission.PROMOTIONS_DELETE_OWN), "OWNER devrait pouvoir supprimer ses promotions");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Role SUPER_ADMIN devrait avoir toutes les permissions")
|
||||||
|
void testSuperAdminHasAllPermissions() {
|
||||||
|
Set<Permission> superAdminPermissions = RolePermissionConfig.getPermissions(Role.SUPER_ADMIN);
|
||||||
|
|
||||||
|
// SUPER_ADMIN devrait avoir accès à tout
|
||||||
|
assertTrue(superAdminPermissions.contains(Permission.SUPER_ADMIN_ACCESS), "SUPER_ADMIN devrait avoir accès super admin");
|
||||||
|
assertTrue(superAdminPermissions.contains(Permission.USERS_DELETE_ANY), "SUPER_ADMIN devrait pouvoir supprimer tout utilisateur");
|
||||||
|
assertTrue(superAdminPermissions.contains(Permission.EVENTS_DELETE_ANY), "SUPER_ADMIN devrait pouvoir supprimer tout événement");
|
||||||
|
assertTrue(superAdminPermissions.contains(Permission.ESTABLISHMENTS_DELETE_ANY), "SUPER_ADMIN devrait pouvoir supprimer tout établissement");
|
||||||
|
assertTrue(superAdminPermissions.contains(Permission.ADMIN_SETTINGS), "SUPER_ADMIN devrait pouvoir modifier les paramètres système");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Role MODERATOR devrait avoir les permissions de modération")
|
||||||
|
void testModeratorRoleHasModerationPermissions() {
|
||||||
|
Set<Permission> modPermissions = RolePermissionConfig.getPermissions(Role.MODERATOR);
|
||||||
|
|
||||||
|
assertTrue(modPermissions.contains(Permission.MODERATION_VIEW_REPORTS), "MODERATOR devrait voir les signalements");
|
||||||
|
assertTrue(modPermissions.contains(Permission.MODERATION_HANDLE_REPORTS), "MODERATOR devrait traiter les signalements");
|
||||||
|
assertTrue(modPermissions.contains(Permission.MODERATION_HIDE_CONTENT), "MODERATOR devrait pouvoir masquer du contenu");
|
||||||
|
assertTrue(modPermissions.contains(Permission.POSTS_MODERATE), "MODERATOR devrait pouvoir modérer les posts");
|
||||||
|
assertTrue(modPermissions.contains(Permission.REVIEWS_MODERATE), "MODERATOR devrait pouvoir modérer les avis");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests du contexte de sécurité avec @TestSecurity
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "testuser@test.com", roles = {"USER"})
|
||||||
|
@DisplayName("Utilisateur authentifié avec rôle USER")
|
||||||
|
void testAuthenticatedUserRole() {
|
||||||
|
assertTrue(securityService.hasAnyRole("USER"), "Devrait avoir le rôle USER");
|
||||||
|
assertFalse(securityService.hasAnyRole("ADMIN"), "Ne devrait pas avoir le rôle ADMIN");
|
||||||
|
assertFalse(securityService.hasAnyRole("SUPER_ADMIN"), "Ne devrait pas avoir le rôle SUPER_ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||||
|
@DisplayName("Utilisateur authentifié avec rôle ADMIN")
|
||||||
|
void testAuthenticatedAdminRole() {
|
||||||
|
assertTrue(securityService.hasAnyRole("ADMIN"), "Devrait avoir le rôle ADMIN");
|
||||||
|
assertTrue(securityService.hasAnyRole("USER", "ADMIN"), "Devrait avoir USER ou ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "superadmin@test.com", roles = {"SUPER_ADMIN"})
|
||||||
|
@DisplayName("Utilisateur authentifié avec rôle SUPER_ADMIN")
|
||||||
|
void testAuthenticatedSuperAdminRole() {
|
||||||
|
assertTrue(securityService.hasAnyRole("SUPER_ADMIN"), "Devrait avoir le rôle SUPER_ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests des utilitaires de rôle
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Test conversion String vers Role")
|
||||||
|
void testRoleFromString() {
|
||||||
|
assertEquals(Role.USER, Role.valueOf("USER"));
|
||||||
|
assertEquals(Role.OWNER, Role.valueOf("OWNER"));
|
||||||
|
assertEquals(Role.MANAGER, Role.valueOf("MANAGER"));
|
||||||
|
assertEquals(Role.ADMIN, Role.valueOf("ADMIN"));
|
||||||
|
assertEquals(Role.SUPER_ADMIN, Role.valueOf("SUPER_ADMIN"));
|
||||||
|
assertEquals(Role.MODERATOR, Role.valueOf("MODERATOR"));
|
||||||
|
assertEquals(Role.SUPPORT, Role.valueOf("SUPPORT"));
|
||||||
|
assertEquals(Role.FINANCE, Role.valueOf("FINANCE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Test que chaque rôle a au moins une permission")
|
||||||
|
void testAllRolesHavePermissions() {
|
||||||
|
for (Role role : Role.values()) {
|
||||||
|
Set<Permission> permissions = RolePermissionConfig.getPermissions(role);
|
||||||
|
assertFalse(permissions.isEmpty(), "Le rôle " + role + " devrait avoir au moins une permission");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Tests des permissions
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Test que toutes les permissions ont une description")
|
||||||
|
void testAllPermissionsHaveDescription() {
|
||||||
|
for (Permission permission : Permission.values()) {
|
||||||
|
assertNotNull(permission.getDescription(), "La permission " + permission + " devrait avoir une description");
|
||||||
|
assertFalse(permission.getDescription().isBlank(), "La description de " + permission + " ne devrait pas être vide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user