feat: v2.0 – réorg docker/scripts, prod, résas, abonnements Wave, Flyway base vierge
This commit is contained in:
@@ -42,3 +42,7 @@ logs/
|
|||||||
*.temp
|
*.temp
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
|
# Scripts et Docker (hors contexte utile pour le build)
|
||||||
|
scripts/
|
||||||
|
docker/
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ DB_USERNAME: afterwork # Utilisateur de la base de données
|
|||||||
DB_PASSWORD: <secret> # Mot de passe (à définir dans le secret)
|
DB_PASSWORD: <secret> # Mot de passe (à définir dans le secret)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Dockerfile.prod
|
### 2. docker/Dockerfile.prod
|
||||||
|
|
||||||
Le fichier `Dockerfile.prod` utilise une approche multi-stage :
|
Le fichier `docker/Dockerfile.prod` utilise une approche multi-stage :
|
||||||
- **Stage 1** : Build avec Maven dans une image UBI8 OpenJDK 17
|
- **Stage 1** : Build avec Maven dans une image UBI8 OpenJDK 17
|
||||||
- **Stage 2** : Runtime optimisé avec l'uber-jar compilé
|
- **Stage 2** : Runtime optimisé avec l'uber-jar compilé
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ Configuration production avec :
|
|||||||
### Build Local (Test)
|
### Build Local (Test)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build de l'image
|
# Build de l'image (Dockerfiles dans docker/)
|
||||||
docker build -f Dockerfile.prod -t afterwork-api:latest .
|
docker build -f docker/Dockerfile.prod -t afterwork-api:latest .
|
||||||
|
|
||||||
# Test local
|
# Test local
|
||||||
docker run -p 8080:8080 \
|
docker run -p 8080:8080 \
|
||||||
@@ -284,8 +284,8 @@ ls target/*-runner.jar
|
|||||||
### Étape 2 : Build Docker
|
### Étape 2 : Build Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build l'image de production
|
# Build l'image de production (Dockerfiles dans docker/)
|
||||||
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
|
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
|
||||||
|
|
||||||
# Test local (optionnel)
|
# Test local (optionnel)
|
||||||
docker run --rm -p 8080:8080 \
|
docker run --rm -p 8080:8080 \
|
||||||
@@ -361,7 +361,7 @@ curl https://api.lions.dev/afterwork/api/users/test
|
|||||||
```bash
|
```bash
|
||||||
# 1. Build nouvelle version
|
# 1. Build nouvelle version
|
||||||
mvn clean package -DskipTests
|
mvn clean package -DskipTests
|
||||||
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.1 .
|
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.1 .
|
||||||
docker push registry.lions.dev/afterwork-api:1.0.1
|
docker push registry.lions.dev/afterwork-api:1.0.1
|
||||||
|
|
||||||
# 2. Mise à jour du déploiement
|
# 2. Mise à jour du déploiement
|
||||||
|
|||||||
257
DIAGNOSTIC_KAFKA_WEBSOCKET.md
Normal file
257
DIAGNOSTIC_KAFKA_WEBSOCKET.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# 🔍 DIAGNOSTIC COMPLET - Kafka & WebSocket Temps Réel
|
||||||
|
|
||||||
|
**Date** : 28 janvier 2026
|
||||||
|
**Problème** : Messagerie, notifications et actualisations événementielles en temps réel ne fonctionnent pas avec Kafka
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 RÉSUMÉ EXÉCUTIF
|
||||||
|
|
||||||
|
L'architecture temps réel utilise Kafka comme bus de messages entre les services métier et les WebSockets. Plusieurs problèmes critiques empêchent le bon fonctionnement :
|
||||||
|
|
||||||
|
1. ❌ **Heartbeat non démarré** côté Flutter
|
||||||
|
2. ⚠️ **Serializers Kafka manquants** (Quarkus auto-génère mais peut échouer)
|
||||||
|
3. ⚠️ **Configuration Kafka incomplète** (bootstrap servers, health checks)
|
||||||
|
4. ⚠️ **WebSocket paths** avec root-path `/afterwork`
|
||||||
|
5. ⚠️ **Bridges Kafka** peuvent ne pas démarrer si Kafka indisponible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 PROBLÈMES CRITIQUES IDENTIFIÉS
|
||||||
|
|
||||||
|
### 1. Heartbeat Non Démarré (Flutter)
|
||||||
|
|
||||||
|
**Fichier** : `afterwork/lib/data/services/realtime_notification_service.dart`
|
||||||
|
|
||||||
|
**Problème** : La méthode `_startHeartbeat()` existe mais n'est **jamais appelée** après une connexion réussie.
|
||||||
|
|
||||||
|
**Ligne 88-101** : Après `_isConnected = true`, le heartbeat n'est pas démarré.
|
||||||
|
|
||||||
|
**Impact** :
|
||||||
|
- La connexion WebSocket peut timeout côté serveur
|
||||||
|
- Le statut de présence n'est pas maintenu
|
||||||
|
- Les notifications peuvent être perdues
|
||||||
|
|
||||||
|
**Solution** : Appeler `_startHeartbeat()` après la connexion réussie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Serializers Kafka Non Explicites
|
||||||
|
|
||||||
|
**Fichier** : `application.properties`
|
||||||
|
|
||||||
|
**Problème** : Les serializers/deserializers sont omis avec le commentaire "Quarkus génère automatiquement". Cependant :
|
||||||
|
- Quarkus utilise Jackson pour sérialiser les DTOs
|
||||||
|
- Les DTOs doivent être correctement annotés
|
||||||
|
- En cas d'échec, les messages ne sont pas publiés dans Kafka
|
||||||
|
|
||||||
|
**Solution** : Ajouter explicitement les serializers Jackson ou vérifier que les DTOs sont sérialisables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Configuration Kafka Bootstrap Servers
|
||||||
|
|
||||||
|
**Fichier** : `application.properties` et `application-prod.properties`
|
||||||
|
|
||||||
|
**Problème** :
|
||||||
|
- Production : `kafka-service.kafka.svc.cluster.local:9092` (Kubernetes DNS)
|
||||||
|
- Dev : `localhost:9092` (override dans `application-dev.properties`)
|
||||||
|
|
||||||
|
**Vérifications nécessaires** :
|
||||||
|
- Kafka est-il déployé en production ?
|
||||||
|
- Le service DNS `kafka-service.kafka.svc.cluster.local` est-il résolvable ?
|
||||||
|
- Les health checks Kafka sont-ils activés ?
|
||||||
|
|
||||||
|
**Solution** : Vérifier le déploiement Kafka et ajouter des health checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. WebSocket Paths avec Root-Path
|
||||||
|
|
||||||
|
**Fichier** : `application-prod.properties`
|
||||||
|
|
||||||
|
**Problème** : `quarkus.http.root-path=/afterwork` est configuré.
|
||||||
|
|
||||||
|
**Impact potentiel** :
|
||||||
|
- Les WebSockets peuvent nécessiter le path complet : `wss://api.lions.dev/afterwork/notifications/{userId}`
|
||||||
|
- Le frontend Flutter utilise : `ws://{baseUrl}/notifications/{userId}`
|
||||||
|
|
||||||
|
**Vérification** : Tester si les WebSockets fonctionnent avec ou sans le root-path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Bridges Kafka Peuvent Échouer Silencieusement
|
||||||
|
|
||||||
|
**Fichiers** : `NotificationKafkaBridge.java`, `ChatKafkaBridge.java`, etc.
|
||||||
|
|
||||||
|
**Problème** : Si Kafka n'est pas disponible au démarrage :
|
||||||
|
- Les bridges peuvent ne pas démarrer
|
||||||
|
- Aucune erreur visible si les exceptions sont catchées
|
||||||
|
- Les messages sont perdus sans notification
|
||||||
|
|
||||||
|
**Solution** : Ajouter des logs de démarrage et vérifier que les bridges sont actifs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 PROBLÈMES MOYENS
|
||||||
|
|
||||||
|
### 6. DTOs Événements Sans Annotations Jackson
|
||||||
|
|
||||||
|
**Fichiers** : `NotificationEvent.java`, `ChatMessageEvent.java`, etc.
|
||||||
|
|
||||||
|
**Problème** : Les DTOs utilisent Lombok mais peuvent manquer d'annotations Jackson pour la sérialisation JSON.
|
||||||
|
|
||||||
|
**Solution** : Vérifier que les DTOs sont correctement sérialisables avec Jackson.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Gestion d'Erreurs Kafka Silencieuse
|
||||||
|
|
||||||
|
**Fichiers** : `MessageService.java`, `EventService.java`, etc.
|
||||||
|
|
||||||
|
**Problème** : Les erreurs Kafka sont catchées et loggées mais ne remontent pas :
|
||||||
|
```java
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage());
|
||||||
|
// Ne pas bloquer l'envoi du message si Kafka échoue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : Les messages sont sauvegardés en DB mais pas diffusés en temps réel, sans notification à l'utilisateur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ CORRECTIONS À APPLIQUER
|
||||||
|
|
||||||
|
### Correction 1 : Démarrer le Heartbeat après Connexion
|
||||||
|
|
||||||
|
**Fichier** : `afterwork/lib/data/services/realtime_notification_service.dart`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
_isConnected = true;
|
||||||
|
_startHeartbeat(); // ← AJOUTER CETTE LIGNE
|
||||||
|
if (!_isDisposed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Correction 2 : Ajouter Serializers Kafka Explicites
|
||||||
|
|
||||||
|
**Fichier** : `application.properties`
|
||||||
|
|
||||||
|
Ajouter pour chaque topic outgoing :
|
||||||
|
```properties
|
||||||
|
mp.messaging.outgoing.notifications.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||||
|
mp.messaging.outgoing.chat-messages.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||||
|
mp.messaging.outgoing.reactions.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||||
|
mp.messaging.outgoing.presence.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
|
||||||
|
```
|
||||||
|
|
||||||
|
Et pour chaque topic incoming :
|
||||||
|
```properties
|
||||||
|
mp.messaging.incoming.kafka-notifications.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||||
|
mp.messaging.incoming.kafka-chat.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||||
|
mp.messaging.incoming.kafka-reactions.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||||
|
mp.messaging.incoming.kafka-presence.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** : Quarkus peut utiliser Jackson au lieu de Jsonb. Vérifier la dépendance dans `pom.xml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Correction 3 : Vérifier les DTOs avec Annotations Jackson
|
||||||
|
|
||||||
|
**Fichiers** : Tous les DTOs d'événements
|
||||||
|
|
||||||
|
Ajouter si nécessaire :
|
||||||
|
```java
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class NotificationEvent {
|
||||||
|
@JsonProperty("userId")
|
||||||
|
private String userId;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Correction 4 : Ajouter Logs de Démarrage des Bridges
|
||||||
|
|
||||||
|
**Fichiers** : Tous les bridges Kafka
|
||||||
|
|
||||||
|
Ajouter dans chaque bridge :
|
||||||
|
```java
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: notifications");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Correction 5 : Améliorer la Gestion d'Erreurs Kafka
|
||||||
|
|
||||||
|
**Fichiers** : Services qui publient dans Kafka
|
||||||
|
|
||||||
|
Au lieu de :
|
||||||
|
```java
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("[ERROR] Erreur Kafka");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Utiliser :
|
||||||
|
```java
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[ERROR] Erreur publication Kafka", e);
|
||||||
|
// Optionnel: Notifier l'utilisateur ou retry
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 TESTS DE VALIDATION
|
||||||
|
|
||||||
|
### Test 1 : Vérifier Connexion Kafka
|
||||||
|
```bash
|
||||||
|
# En production (Kubernetes)
|
||||||
|
kubectl exec -it kafka-pod -- kafka-console-consumer --bootstrap-server localhost:9092 --topic notifications --from-beginning
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2 : Vérifier WebSocket
|
||||||
|
```javascript
|
||||||
|
// Dans la console navigateur
|
||||||
|
const ws = new WebSocket('wss://api.lions.dev/afterwork/notifications/{userId}');
|
||||||
|
ws.onopen = () => console.log('Connected');
|
||||||
|
ws.onmessage = (e) => console.log('Message:', e.data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3 : Vérifier Heartbeat Flutter
|
||||||
|
- Ouvrir les logs Flutter
|
||||||
|
- Vérifier que des "Ping envoyé" apparaissent toutes les 30 secondes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 CHECKLIST DE VÉRIFICATION
|
||||||
|
|
||||||
|
- [ ] Kafka est déployé et accessible
|
||||||
|
- [ ] Les topics Kafka existent (`notifications`, `chat.messages`, `reactions`, `presence.updates`)
|
||||||
|
- [ ] Les bridges Kafka démarrent sans erreur
|
||||||
|
- [ ] Les WebSockets se connectent correctement
|
||||||
|
- [ ] Le heartbeat Flutter fonctionne
|
||||||
|
- [ ] Les messages sont publiés dans Kafka
|
||||||
|
- [ ] Les messages sont routés vers WebSocket
|
||||||
|
- [ ] Les clients reçoivent les notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 RESSOURCES
|
||||||
|
|
||||||
|
- [Quarkus Reactive Messaging Kafka](https://quarkus.io/guides/kafka)
|
||||||
|
- [Quarkus WebSockets Next](https://quarkus.io/guides/websockets-next)
|
||||||
|
- [SmallRye Reactive Messaging](https://smallrye.io/smallrye-reactive-messaging/)
|
||||||
@@ -8,15 +8,15 @@
|
|||||||
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
||||||
|
|
||||||
# Déploiement complet (build + push + deploy)
|
# Déploiement complet (build + push + deploy)
|
||||||
.\deploy.ps1 -Action all -Version 1.0.0
|
.\scripts\deploy.ps1 -Action all -Version 1.0.0
|
||||||
|
|
||||||
# Ou étape par étape
|
# Ou étape par étape
|
||||||
.\deploy.ps1 -Action build # Build Maven + Docker
|
.\scripts\deploy.ps1 -Action build # Build Maven + Docker
|
||||||
.\deploy.ps1 -Action push # Push vers registry
|
.\scripts\deploy.ps1 -Action push # Push vers registry
|
||||||
.\deploy.ps1 -Action deploy # Déploiement K8s
|
.\scripts\deploy.ps1 -Action deploy # Déploiement K8s
|
||||||
|
|
||||||
# Vérifier le statut
|
# Vérifier le statut
|
||||||
.\deploy.ps1 -Action status
|
.\scripts\deploy.ps1 -Action status
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2 : Déploiement Manuel
|
### Option 2 : Déploiement Manuel
|
||||||
@@ -28,7 +28,7 @@ cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
|||||||
mvn clean package -DskipTests
|
mvn clean package -DskipTests
|
||||||
|
|
||||||
# 2. Build Docker
|
# 2. Build Docker
|
||||||
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 -t registry.lions.dev/afterwork-api:latest .
|
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 -t registry.lions.dev/afterwork-api:latest .
|
||||||
|
|
||||||
# 3. Push vers Registry
|
# 3. Push vers Registry
|
||||||
docker login registry.lions.dev
|
docker login registry.lions.dev
|
||||||
@@ -55,7 +55,7 @@ cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
|||||||
|
|
||||||
# Build local
|
# Build local
|
||||||
mvn clean package -DskipTests
|
mvn clean package -DskipTests
|
||||||
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
|
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
|
||||||
docker push registry.lions.dev/afterwork-api:1.0.0
|
docker push registry.lions.dev/afterwork-api:1.0.0
|
||||||
|
|
||||||
# Déploiement
|
# Déploiement
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:13
|
|
||||||
container_name: afterwork_db
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: "${DB_USERNAME}"
|
|
||||||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
|
||||||
POSTGRES_DB: "${DB_NAME}"
|
|
||||||
networks:
|
|
||||||
- afterwork-network
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
app:
|
|
||||||
image: dahoudg/afterwork-quarkus:latest
|
|
||||||
container_name: afterwork-quarkus
|
|
||||||
environment:
|
|
||||||
DB_USERNAME: "${DB_USERNAME}"
|
|
||||||
DB_PASSWORD: "${DB_PASSWORD}"
|
|
||||||
DB_HOST: "${DB_HOST}"
|
|
||||||
DB_PORT: "${DB_PORT}"
|
|
||||||
DB_NAME: "${DB_NAME}"
|
|
||||||
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
|
||||||
- afterwork-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
swagger-ui:
|
|
||||||
image: swaggerapi/swagger-ui
|
|
||||||
container_name: afterwork-swagger-ui
|
|
||||||
environment:
|
|
||||||
SWAGGER_JSON: http://app:8080/openapi
|
|
||||||
ports:
|
|
||||||
- "8081:8080"
|
|
||||||
depends_on:
|
|
||||||
- app
|
|
||||||
networks:
|
|
||||||
- afterwork-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
|
||||||
afterwork-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data:
|
|
||||||
driver: local
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
##
|
##
|
||||||
## AfterWork Server - Development Dockerfile
|
## AfterWork Server - Development Dockerfile
|
||||||
## Image légère avec JRE Alpine
|
## Image légère avec JRE Alpine (JAR pré-buildé requis)
|
||||||
##
|
##
|
||||||
|
|
||||||
FROM eclipse-temurin:17-jre-alpine
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
@@ -25,7 +25,7 @@ RUN mkdir -p /app /tmp/uploads && \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copie du JAR
|
# Copie du JAR (context = racine du projet, build après mvn package)
|
||||||
COPY --chown=appuser:appuser target/*-runner.jar /app/app.jar
|
COPY --chown=appuser:appuser target/*-runner.jar /app/app.jar
|
||||||
|
|
||||||
# Exposition du port
|
# Exposition du port
|
||||||
@@ -13,7 +13,7 @@ USER root
|
|||||||
# Installation de Maven
|
# Installation de Maven
|
||||||
RUN microdnf install -y maven && microdnf clean all
|
RUN microdnf install -y maven && microdnf clean all
|
||||||
|
|
||||||
# Copie des fichiers du projet
|
# Copie des fichiers du projet (context = racine du projet)
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY pom.xml .
|
COPY pom.xml .
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
54
docker/README.md
Normal file
54
docker/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Docker AfterWork
|
||||||
|
|
||||||
|
Fichiers Docker pour le build et l’exécution de l’API AfterWork.
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
| Fichier | Usage |
|
||||||
|
|---------|--------|
|
||||||
|
| `Dockerfile` | Image dev (JAR pré-buildé, Alpine) |
|
||||||
|
| `Dockerfile.prod` | Image prod (multi-stage Maven + UBI8) |
|
||||||
|
| `docker-compose.yml` | Stack optionnelle (app + PostgreSQL/Kafka sur l’hôte) |
|
||||||
|
|
||||||
|
## Build (depuis la racine du projet)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Image de production
|
||||||
|
docker build -f docker/Dockerfile.prod -t afterwork-quarkus:latest .
|
||||||
|
|
||||||
|
# Image de dev (après mvn package)
|
||||||
|
docker build -f docker/Dockerfile -t afterwork-quarkus:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
Utilise le PostgreSQL et Kafka déjà en cours d’exécution sur l’hôte (host.docker.internal).
|
||||||
|
|
||||||
|
**Depuis la racine :**
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Depuis docker/ :**
|
||||||
|
```bash
|
||||||
|
cd docker && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL (obligatoire)
|
||||||
|
|
||||||
|
L’application se connecte à PostgreSQL sur l’hôte (`host.docker.internal:5432`). Sans identifiants, l’erreur **« no password was provided »** apparaît.
|
||||||
|
|
||||||
|
- **Par défaut** (si vous ne définissez rien) : `DB_USERNAME=afterwork`, `DB_PASSWORD=changeme`, `DB_NAME=afterwork_db`.
|
||||||
|
- Créer la base et l’utilisateur dans PostgreSQL, par exemple :
|
||||||
|
```sql
|
||||||
|
CREATE USER afterwork WITH PASSWORD 'changeme';
|
||||||
|
CREATE DATABASE afterwork_db OWNER afterwork;
|
||||||
|
```
|
||||||
|
- Ou utiliser **vos** identifiants via un fichier **`.env` à la racine du projet** (mic-after-work-server-impl-quarkus-main) — Docker Compose le charge quand vous lancez depuis cette racine :
|
||||||
|
```bash
|
||||||
|
# Contenu de .env à la racine du projet
|
||||||
|
DB_USERNAME=monuser
|
||||||
|
DB_PASSWORD=monmotdepasse
|
||||||
|
DB_NAME=afterwork_db
|
||||||
|
```
|
||||||
|
Si vous lancez depuis `docker/` (`cd docker && docker-compose up`), placez le `.env` dans le dossier `docker/`.
|
||||||
26
docker/docker-compose.yml
Normal file
26
docker/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Dev: mvn quarkus:dev (H2 in-memory, Swagger /q/swagger-ui, Kafka localhost:9092)
|
||||||
|
# App utilise le PostgreSQL existant (ex: skyfile sur 5432) - créer la DB: CREATE DATABASE afterwork_db;
|
||||||
|
# Lancer depuis la racine: docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
# Ou depuis docker/: docker-compose up -d
|
||||||
|
#
|
||||||
|
# PostgreSQL: définir DB_USERNAME/DB_PASSWORD si votre instance utilise d'autres identifiants.
|
||||||
|
# Exemple .env à la racine docker/ : DB_USERNAME=monuser DB_PASSWORD=monmotdepasse
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile.prod
|
||||||
|
image: afterwork-quarkus:latest
|
||||||
|
container_name: afterwork-quarkus
|
||||||
|
environment:
|
||||||
|
QUARKUS_PROFILE: prod
|
||||||
|
DB_HOST: host.docker.internal
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_NAME: "${DB_NAME:-afterwork_db}"
|
||||||
|
DB_USERNAME: "${DB_USERNAME:-afterwork}"
|
||||||
|
DB_PASSWORD: "${DB_PASSWORD:-changeme}"
|
||||||
|
KAFKA_BOOTSTRAP_SERVERS: host.docker.internal:9092
|
||||||
|
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -46,9 +46,8 @@ metadata:
|
|||||||
nginx.ingress.kubernetes.io/rate-limit: "1000"
|
nginx.ingress.kubernetes.io/rate-limit: "1000"
|
||||||
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
|
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
|
||||||
|
|
||||||
# Rewrite (important pour /afterwork)
|
# PAS de rewrite-target : le backend sert sous quarkus.http.root-path=/afterwork,
|
||||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
# l'Ingress doit transmettre le chemin complet (/afterwork/...) au service.
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: nginx
|
ingressClassName: nginx
|
||||||
@@ -60,8 +59,8 @@ spec:
|
|||||||
- host: api.lions.dev
|
- host: api.lions.dev
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
- path: /afterwork(/|$)(.*)
|
- path: /afterwork
|
||||||
pathType: ImplementationSpecific
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: mic-after-work-server-impl-quarkus-main-service
|
name: mic-after-work-server-impl-quarkus-main-service
|
||||||
|
|||||||
20
scripts/README.md
Normal file
20
scripts/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Scripts AfterWork
|
||||||
|
|
||||||
|
Scripts de déploiement et d’outillage.
|
||||||
|
|
||||||
|
## deploy.ps1
|
||||||
|
|
||||||
|
Script PowerShell de déploiement production (build Maven, build Docker, push registry, déploiement Kubernetes).
|
||||||
|
|
||||||
|
**Exécution** (depuis la racine du projet) :
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\deploy.ps1 -Action all -Version 1.0.0
|
||||||
|
.\scripts\deploy.ps1 -Action build # Build Maven + Docker
|
||||||
|
.\scripts\deploy.ps1 -Action push # Push vers registry
|
||||||
|
.\scripts\deploy.ps1 -Action deploy # Déploiement K8s
|
||||||
|
.\scripts\deploy.ps1 -Action status # Statut du déploiement
|
||||||
|
.\scripts\deploy.ps1 -Action rollback # Rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
Le script détecte automatiquement la racine du projet (parent de `scripts/`).
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Ce script automatise le processus de build et déploiement
|
# Ce script automatise le processus de build et déploiement
|
||||||
# de l'API AfterWork sur le VPS via Kubernetes.
|
# de l'API AfterWork sur le VPS via Kubernetes.
|
||||||
|
# Exécuter depuis la racine du projet ou depuis scripts/
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
|
|
||||||
param(
|
param(
|
||||||
@@ -20,6 +21,10 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Racine du projet (parent du dossier scripts)
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$ProjectRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
|
||||||
|
|
||||||
# Couleurs
|
# Couleurs
|
||||||
function Write-Info { param($msg) Write-Host $msg -ForegroundColor Cyan }
|
function Write-Info { param($msg) Write-Host $msg -ForegroundColor Cyan }
|
||||||
function Write-Success { param($msg) Write-Host $msg -ForegroundColor Green }
|
function Write-Success { param($msg) Write-Host $msg -ForegroundColor Green }
|
||||||
@@ -42,6 +47,7 @@ Write-Host " - Version: $Version"
|
|||||||
Write-Host " - Registry: $Registry"
|
Write-Host " - Registry: $Registry"
|
||||||
Write-Host " - Image: $ImageName"
|
Write-Host " - Image: $ImageName"
|
||||||
Write-Host " - Namespace: $Namespace"
|
Write-Host " - Namespace: $Namespace"
|
||||||
|
Write-Host " - Racine projet: $ProjectRoot"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@@ -50,27 +56,31 @@ Write-Host ""
|
|||||||
function Build-Application {
|
function Build-Application {
|
||||||
Write-Info "[1/5] Build Maven..."
|
Write-Info "[1/5] Build Maven..."
|
||||||
|
|
||||||
$mavenArgs = "clean", "package", "-Dquarkus.package.type=uber-jar"
|
Push-Location $ProjectRoot
|
||||||
if ($SkipTests) {
|
try {
|
||||||
$mavenArgs += "-DskipTests"
|
$mavenArgs = "clean", "package", "-Dquarkus.package.type=uber-jar"
|
||||||
} else {
|
if ($SkipTests) {
|
||||||
$mavenArgs += "-DtestFailureIgnore=true"
|
$mavenArgs += "-DskipTests"
|
||||||
}
|
} else {
|
||||||
|
$mavenArgs += "-DtestFailureIgnore=true"
|
||||||
|
}
|
||||||
|
|
||||||
& mvn $mavenArgs
|
& mvn $mavenArgs
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Erreur lors du build Maven"
|
Write-Error "Erreur lors du build Maven"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vérifier que le JAR existe
|
$jar = Get-ChildItem -Path (Join-Path $ProjectRoot "target") -Filter "*-runner.jar" | Select-Object -First 1
|
||||||
$jar = Get-ChildItem -Path "target" -Filter "*-runner.jar" | Select-Object -First 1
|
if (-not $jar) {
|
||||||
if (-not $jar) {
|
Write-Error "JAR runner non trouvé dans target/"
|
||||||
Write-Error "JAR runner non trouvé dans target/"
|
exit 1
|
||||||
exit 1
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Write-Success "Build Maven réussi : $($jar.Name)"
|
Write-Success "Build Maven réussi : $($jar.Name)"
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@@ -79,13 +89,19 @@ function Build-Application {
|
|||||||
function Build-DockerImage {
|
function Build-DockerImage {
|
||||||
Write-Info "[2/5] Build Docker Image..."
|
Write-Info "[2/5] Build Docker Image..."
|
||||||
|
|
||||||
docker build -f Dockerfile.prod -t $ImageName -t $ImageLatest .
|
Push-Location $ProjectRoot
|
||||||
if ($LASTEXITCODE -ne 0) {
|
try {
|
||||||
Write-Error "Erreur lors du build Docker"
|
$dockerDir = Join-Path $ProjectRoot "docker"
|
||||||
exit 1
|
docker build -f (Join-Path $dockerDir "Dockerfile.prod") -t $ImageName -t $ImageLatest .
|
||||||
}
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Erreur lors du build Docker"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
Write-Success "Image Docker créée : $ImageName"
|
Write-Success "Image Docker créée : $ImageName"
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@@ -94,7 +110,6 @@ function Build-DockerImage {
|
|||||||
function Push-ToRegistry {
|
function Push-ToRegistry {
|
||||||
Write-Info "[3/5] Push vers Registry..."
|
Write-Info "[3/5] Push vers Registry..."
|
||||||
|
|
||||||
# Vérifier si on est connecté au registry
|
|
||||||
$loginTest = docker login $Registry 2>&1
|
$loginTest = docker login $Registry 2>&1
|
||||||
if ($LASTEXITCODE -ne 0 -and -not $loginTest.ToString().Contains("Succeeded")) {
|
if ($LASTEXITCODE -ne 0 -and -not $loginTest.ToString().Contains("Succeeded")) {
|
||||||
Write-Warning "Connexion au registry nécessaire..."
|
Write-Warning "Connexion au registry nécessaire..."
|
||||||
@@ -105,7 +120,6 @@ function Push-ToRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Push des images
|
|
||||||
docker push $ImageName
|
docker push $ImageName
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Erreur lors du push de $ImageName"
|
Write-Error "Erreur lors du push de $ImageName"
|
||||||
@@ -127,20 +141,18 @@ function Push-ToRegistry {
|
|||||||
function Deploy-ToKubernetes {
|
function Deploy-ToKubernetes {
|
||||||
Write-Info "[4/5] Déploiement Kubernetes..."
|
Write-Info "[4/5] Déploiement Kubernetes..."
|
||||||
|
|
||||||
# Vérifier que kubectl est disponible
|
|
||||||
$kubectlCheck = kubectl version --client 2>&1
|
$kubectlCheck = kubectl version --client 2>&1
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "kubectl n'est pas installé ou configuré"
|
Write-Error "kubectl n'est pas installé ou configuré"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Créer le namespace si nécessaire
|
$k8sDir = Join-Path $ProjectRoot "kubernetes"
|
||||||
Write-Info "Création du namespace $Namespace..."
|
Write-Info "Création du namespace $Namespace..."
|
||||||
kubectl create namespace $Namespace --dry-run=client -o yaml | kubectl apply -f -
|
kubectl create namespace $Namespace --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
# Appliquer les manifests
|
|
||||||
Write-Info "Application des ConfigMaps et Secrets..."
|
Write-Info "Application des ConfigMaps et Secrets..."
|
||||||
kubectl apply -f kubernetes/afterwork-configmap.yaml
|
kubectl apply -f (Join-Path $k8sDir "afterwork-configmap.yaml")
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Warning "ConfigMap déjà existante ou erreur"
|
Write-Warning "ConfigMap déjà existante ou erreur"
|
||||||
}
|
}
|
||||||
@@ -155,26 +167,26 @@ function Deploy-ToKubernetes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kubectl apply -f kubernetes/afterwork-secrets.yaml
|
kubectl apply -f (Join-Path $k8sDir "afterwork-secrets.yaml")
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Erreur lors de l'application des secrets"
|
Write-Error "Erreur lors de l'application des secrets"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Info "Déploiement de l'application..."
|
Write-Info "Déploiement de l'application..."
|
||||||
kubectl apply -f kubernetes/afterwork-deployment.yaml
|
kubectl apply -f (Join-Path $k8sDir "afterwork-deployment.yaml")
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Erreur lors du déploiement"
|
Write-Error "Erreur lors du déploiement"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
kubectl apply -f kubernetes/afterwork-service.yaml
|
kubectl apply -f (Join-Path $k8sDir "afterwork-service.yaml")
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Erreur lors de la création du service"
|
Write-Error "Erreur lors de la création du service"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
kubectl apply -f kubernetes/afterwork-ingress.yaml
|
kubectl apply -f (Join-Path $k8sDir "afterwork-ingress.yaml")
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "Erreur lors de la création de l'ingress"
|
Write-Error "Erreur lors de la création de l'ingress"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -182,7 +194,6 @@ function Deploy-ToKubernetes {
|
|||||||
|
|
||||||
Write-Success "Déploiement Kubernetes réussi"
|
Write-Success "Déploiement Kubernetes réussi"
|
||||||
|
|
||||||
# Attendre que le déploiement soit prêt
|
|
||||||
Write-Info "Attente du rollout..."
|
Write-Info "Attente du rollout..."
|
||||||
kubectl rollout status deployment/$AppName -n $Namespace --timeout=5m
|
kubectl rollout status deployment/$AppName -n $Namespace --timeout=5m
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
@@ -196,19 +207,15 @@ function Deploy-ToKubernetes {
|
|||||||
function Verify-Deployment {
|
function Verify-Deployment {
|
||||||
Write-Info "[5/5] Vérification du déploiement..."
|
Write-Info "[5/5] Vérification du déploiement..."
|
||||||
|
|
||||||
# Status des pods
|
|
||||||
Write-Info "Pods:"
|
Write-Info "Pods:"
|
||||||
kubectl get pods -n $Namespace -l app=$AppName
|
kubectl get pods -n $Namespace -l app=$AppName
|
||||||
|
|
||||||
# Status du service
|
|
||||||
Write-Info "`nService:"
|
Write-Info "`nService:"
|
||||||
kubectl get svc -n $Namespace $AppName
|
kubectl get svc -n $Namespace $AppName
|
||||||
|
|
||||||
# Status de l'ingress
|
|
||||||
Write-Info "`nIngress:"
|
Write-Info "`nIngress:"
|
||||||
kubectl get ingress -n $Namespace $AppName
|
kubectl get ingress -n $Namespace $AppName
|
||||||
|
|
||||||
# Test health check
|
|
||||||
Write-Info "`nTest Health Check..."
|
Write-Info "`nTest Health Check..."
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
59
src/main/java/com/lions/dev/config/SuperAdminStartup.java
Normal file
59
src/main/java/com/lions/dev/config/SuperAdminStartup.java
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.lions.dev.config;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.users.Users;
|
||||||
|
import com.lions.dev.repository.UsersRepository;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import io.quarkus.runtime.StartupEvent;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Observes;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée le super administrateur au démarrage de l'application si aucun n'existe.
|
||||||
|
* Email et mot de passe configurables (variables d'environnement en production).
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SuperAdminStartup {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(SuperAdminStartup.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UsersRepository usersRepository;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.super-admin.email", defaultValue = "superadmin@afterwork.lions.dev")
|
||||||
|
String superAdminEmail;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.super-admin.password", defaultValue = "SuperAdmin2025!")
|
||||||
|
String superAdminPassword;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.super-admin.first-name", defaultValue = "Super")
|
||||||
|
String superAdminFirstName;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.super-admin.last-name", defaultValue = "Administrator")
|
||||||
|
String superAdminLastName;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
void onStart(@Observes StartupEvent event) {
|
||||||
|
if (usersRepository.findByEmail(superAdminEmail).isPresent()) {
|
||||||
|
LOG.info("Super administrateur déjà présent (email: " + superAdminEmail + "). Aucune création.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Users superAdmin = new Users();
|
||||||
|
superAdmin.setFirstName(superAdminFirstName);
|
||||||
|
superAdmin.setLastName(superAdminLastName);
|
||||||
|
superAdmin.setEmail(superAdminEmail);
|
||||||
|
superAdmin.setPassword(superAdminPassword);
|
||||||
|
superAdmin.setRole(UserRoles.SUPER_ADMIN);
|
||||||
|
superAdmin.setProfileImageUrl("https://placehold.co/150x150.png");
|
||||||
|
superAdmin.setBio("Super administrateur AfterWork");
|
||||||
|
superAdmin.setLoyaltyPoints(0);
|
||||||
|
superAdmin.setVerified(true);
|
||||||
|
|
||||||
|
usersRepository.persist(superAdmin);
|
||||||
|
LOG.info("Super administrateur créé au démarrage : " + superAdminEmail + " (role: " + UserRoles.SUPER_ADMIN + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.lions.dev.dto.request.booking;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO de création de réservation (aligné frontend).
|
||||||
|
*/
|
||||||
|
public class ReservationCreateRequestDTO {
|
||||||
|
|
||||||
|
@NotNull(message = "userId est obligatoire")
|
||||||
|
private UUID userId;
|
||||||
|
|
||||||
|
@NotNull(message = "establishmentId est obligatoire")
|
||||||
|
private UUID establishmentId;
|
||||||
|
|
||||||
|
/** Date/heure de réservation (ISO-8601 ou timestamp). */
|
||||||
|
private String reservationDate;
|
||||||
|
|
||||||
|
@Min(1)
|
||||||
|
private int numberOfPeople = 1;
|
||||||
|
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
public UUID getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(UUID userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getEstablishmentId() {
|
||||||
|
return establishmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEstablishmentId(UUID establishmentId) {
|
||||||
|
this.establishmentId = establishmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReservationDate() {
|
||||||
|
return reservationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReservationDate(String reservationDate) {
|
||||||
|
this.reservationDate = reservationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumberOfPeople() {
|
||||||
|
return numberOfPeople;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNumberOfPeople(int numberOfPeople) {
|
||||||
|
this.numberOfPeople = numberOfPeople;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNotes() {
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotes(String notes) {
|
||||||
|
this.notes = notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.lions.dev.dto.request.establishment;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requête pour initier un paiement Wave (droits d'accès établissement).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InitiateSubscriptionRequestDTO {
|
||||||
|
|
||||||
|
/** Plan : MONTHLY, YEARLY */
|
||||||
|
@NotBlank(message = "Le plan est obligatoire")
|
||||||
|
@Pattern(regexp = "MONTHLY|YEARLY", message = "Plan invalide. Valeurs : MONTHLY, YEARLY")
|
||||||
|
private String plan;
|
||||||
|
|
||||||
|
/** Numéro de téléphone client au format international (ex. 221771234567). */
|
||||||
|
@NotBlank(message = "Le numéro de téléphone client est obligatoire pour Wave")
|
||||||
|
private String clientPhone;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.lions.dev.dto.request.friends;
|
package com.lions.dev.dto.request.friends;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -15,7 +16,10 @@ import java.util.UUID;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class FriendshipCreateOneRequestDTO {
|
public class FriendshipCreateOneRequestDTO {
|
||||||
|
|
||||||
|
@NotNull(message = "L'identifiant de l'utilisateur est requis")
|
||||||
private UUID userId; // ID de l'utilisateur qui envoie la demande
|
private UUID userId; // ID de l'utilisateur qui envoie la demande
|
||||||
|
|
||||||
|
@NotNull(message = "L'identifiant de l'ami est requis")
|
||||||
private UUID friendId; // ID de l'utilisateur qui reçoit la demande
|
private UUID friendId; // ID de l'utilisateur qui reçoit la demande
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.lions.dev.dto.request.users;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour l'attribution d'un rôle à un utilisateur (opération réservée au super admin).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class AssignRoleRequestDTO {
|
||||||
|
|
||||||
|
private static final String ROLE_PATTERN = "SUPER_ADMIN|ADMIN|MANAGER|USER";
|
||||||
|
|
||||||
|
@NotBlank(message = "Le rôle est obligatoire")
|
||||||
|
@Pattern(regexp = ROLE_PATTERN, message = "Rôle invalide. Valeurs autorisées : SUPER_ADMIN, ADMIN, MANAGER, USER")
|
||||||
|
private String role;
|
||||||
|
|
||||||
|
public AssignRoleRequestDTO(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.lions.dev.dto.response.admin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AdminRevenueResponseDTO {
|
||||||
|
|
||||||
|
/** Revenus totaux (abonnements actifs * prix). */
|
||||||
|
private BigDecimal totalRevenueXof;
|
||||||
|
/** Nombre d'abonnements actifs. */
|
||||||
|
private long activeSubscriptionsCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.lions.dev.dto.response.admin;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ManagerStatsResponseDTO {
|
||||||
|
|
||||||
|
private UUID userId;
|
||||||
|
private String email;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
/** ACTIVE ou SUSPENDED (isActive). */
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime subscriptionExpiresAt;
|
||||||
|
private UUID establishmentId;
|
||||||
|
private String establishmentName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.lions.dev.dto.response.booking;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.booking.Booking;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO de réponse pour une réservation (aligné sur le frontend Flutter ReservationModel).
|
||||||
|
* eventId/eventTitle : pour les réservations d'établissement, eventTitle = nom de l'établissement, eventId = null.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class ReservationResponseDTO {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String userId;
|
||||||
|
private final String userFullName;
|
||||||
|
private final String eventId; // null pour résa établissement
|
||||||
|
private final String eventTitle; // nom événement ou établissement
|
||||||
|
private final String reservationDate; // ISO-8601
|
||||||
|
private final int numberOfPeople;
|
||||||
|
private final String status; // PENDING, CONFIRMED, CANCELLED, COMPLETED
|
||||||
|
private final String establishmentId;
|
||||||
|
private final String establishmentName;
|
||||||
|
private final String notes;
|
||||||
|
private final String createdAt; // ISO-8601
|
||||||
|
|
||||||
|
public ReservationResponseDTO(Booking booking) {
|
||||||
|
this.id = booking.getId() != null ? booking.getId().toString() : null;
|
||||||
|
this.userId = booking.getUser() != null && booking.getUser().getId() != null
|
||||||
|
? booking.getUser().getId().toString() : null;
|
||||||
|
this.userFullName = booking.getUser() != null
|
||||||
|
? (booking.getUser().getFirstName() + " " + booking.getUser().getLastName()).trim()
|
||||||
|
: "";
|
||||||
|
this.eventId = null; // Réservation établissement sans événement
|
||||||
|
this.eventTitle = booking.getEstablishment() != null ? booking.getEstablishment().getName() : "";
|
||||||
|
this.reservationDate = booking.getReservationTime() != null
|
||||||
|
? booking.getReservationTime().toString() : null;
|
||||||
|
this.numberOfPeople = booking.getGuestCount() != null ? booking.getGuestCount() : 1;
|
||||||
|
this.status = booking.getStatus() != null ? booking.getStatus().toLowerCase() : "pending";
|
||||||
|
this.establishmentId = booking.getEstablishment() != null && booking.getEstablishment().getId() != null
|
||||||
|
? booking.getEstablishment().getId().toString() : null;
|
||||||
|
this.establishmentName = booking.getEstablishment() != null ? booking.getEstablishment().getName() : null;
|
||||||
|
this.notes = booking.getSpecialRequests();
|
||||||
|
this.createdAt = booking.getCreatedAt() != null ? booking.getCreatedAt().toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.lions.dev.dto.response.establishment;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réponse après initiation d'un paiement Wave (URL de redirection).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class InitiateSubscriptionResponseDTO {
|
||||||
|
|
||||||
|
private String paymentUrl;
|
||||||
|
private String waveSessionId;
|
||||||
|
private Integer amountXof;
|
||||||
|
private String plan;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ public class UserCreateResponseDTO {
|
|||||||
private String lastName; // v2.0 - Nom de famille de l'utilisateur
|
private String lastName; // v2.0 - Nom de famille de l'utilisateur
|
||||||
private String email; // Email de l'utilisateur
|
private String email; // Email de l'utilisateur
|
||||||
private String role; // Rôle de l'utilisateur
|
private String role; // Rôle de l'utilisateur
|
||||||
|
private Boolean isActive = true; // false = manager suspendu (abonnement expiré)
|
||||||
private String profileImageUrl; // URL de l'image de profil de l'utilisateur
|
private String profileImageUrl; // URL de l'image de profil de l'utilisateur
|
||||||
private String bio; // v2.0 - Biographie courte
|
private String bio; // v2.0 - Biographie courte
|
||||||
private Integer loyaltyPoints; // v2.0 - Points de fidélité
|
private Integer loyaltyPoints; // v2.0 - Points de fidélité
|
||||||
@@ -56,6 +57,7 @@ public class UserCreateResponseDTO {
|
|||||||
this.lastName = user.getLastName(); // v2.0
|
this.lastName = user.getLastName(); // v2.0
|
||||||
this.email = user.getEmail();
|
this.email = user.getEmail();
|
||||||
this.role = user.getRole();
|
this.role = user.getRole();
|
||||||
|
this.isActive = user.isActive();
|
||||||
this.profileImageUrl = user.getProfileImageUrl();
|
this.profileImageUrl = user.getProfileImageUrl();
|
||||||
this.bio = user.getBio(); // v2.0
|
this.bio = user.getBio(); // v2.0
|
||||||
this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
|
this.loyaltyPoints = user.getLoyaltyPoints(); // v2.0
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ public class Establishment extends BaseEntity {
|
|||||||
@Column(name = "verification_status", nullable = false)
|
@Column(name = "verification_status", nullable = false)
|
||||||
private String verificationStatus = "PENDING"; // Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)
|
private String verificationStatus = "PENDING"; // Statut de vérification: PENDING, VERIFIED, REJECTED (v2.0)
|
||||||
|
|
||||||
|
/** true = visible dans l'app ; false = masqué (abonnement inactif / suspension Wave). Par défaut true. */
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private Boolean isActive = true;
|
||||||
|
|
||||||
@Column(name = "latitude")
|
@Column(name = "latitude")
|
||||||
private Double latitude; // Latitude pour la géolocalisation
|
private Double latitude; // Latitude pour la géolocalisation
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.lions.dev.entity.establishment;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.BaseEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistrement d'un paiement Wave pour un établissement (droits d'accès).
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "establishment_payments")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class EstablishmentPayment extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "establishment_id", nullable = false)
|
||||||
|
private UUID establishmentId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "establishment_id", insertable = false, updatable = false)
|
||||||
|
private Establishment establishment;
|
||||||
|
|
||||||
|
@Column(name = "amount_xof", nullable = false)
|
||||||
|
private Integer amountXof;
|
||||||
|
|
||||||
|
@Column(name = "wave_session_id", length = 255)
|
||||||
|
private String waveSessionId;
|
||||||
|
|
||||||
|
/** Statut : PENDING, COMPLETED, FAILED, CANCELLED */
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status = "PENDING";
|
||||||
|
|
||||||
|
@Column(name = "client_phone", length = 30)
|
||||||
|
private String clientPhone;
|
||||||
|
|
||||||
|
@Column(name = "plan", length = 20)
|
||||||
|
private String plan;
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING = "PENDING";
|
||||||
|
public static final String STATUS_COMPLETED = "COMPLETED";
|
||||||
|
public static final String STATUS_FAILED = "FAILED";
|
||||||
|
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.lions.dev.entity.establishment;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.BaseEntity;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abonnement / droits d'accès d'un établissement (paiement via Wave).
|
||||||
|
* Un établissement doit avoir un abonnement actif pour accéder aux fonctionnalités payantes.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "establishment_subscriptions")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class EstablishmentSubscription extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(name = "establishment_id", nullable = false)
|
||||||
|
private UUID establishmentId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "establishment_id", insertable = false, updatable = false)
|
||||||
|
private Establishment establishment;
|
||||||
|
|
||||||
|
/** Plan : MONTHLY, YEARLY */
|
||||||
|
@Column(name = "plan", nullable = false, length = 20)
|
||||||
|
private String plan;
|
||||||
|
|
||||||
|
/** Statut : PENDING, ACTIVE, EXPIRED, CANCELLED */
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status = "PENDING";
|
||||||
|
|
||||||
|
@Column(name = "wave_session_id", length = 255)
|
||||||
|
private String waveSessionId;
|
||||||
|
|
||||||
|
@Column(name = "amount_xof")
|
||||||
|
private Integer amountXof;
|
||||||
|
|
||||||
|
@Column(name = "paid_at")
|
||||||
|
private LocalDateTime paidAt;
|
||||||
|
|
||||||
|
@Column(name = "expires_at")
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
public static final String PLAN_MONTHLY = "MONTHLY";
|
||||||
|
public static final String PLAN_YEARLY = "YEARLY";
|
||||||
|
public static final String STATUS_PENDING = "PENDING";
|
||||||
|
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
|
public static final String STATUS_EXPIRED = "EXPIRED";
|
||||||
|
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||||
|
}
|
||||||
@@ -27,9 +27,9 @@ public class SocialPost extends BaseEntity {
|
|||||||
@Column(name = "content", nullable = false, length = 2000)
|
@Column(name = "content", nullable = false, length = 2000)
|
||||||
private String content; // Le contenu textuel du post
|
private String content; // Le contenu textuel du post
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
private Users user; // L'utilisateur créateur du post
|
private Users user; // L'utilisateur créateur du post (EAGER pour éviter 500 sur mapping DTO)
|
||||||
|
|
||||||
@Column(name = "image_url", length = 500)
|
@Column(name = "image_url", length = 500)
|
||||||
private String imageUrl; // URL de l'image associée (optionnel)
|
private String imageUrl; // URL de l'image associée (optionnel)
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ public class Users extends BaseEntity {
|
|||||||
@Column(name = "last_seen")
|
@Column(name = "last_seen")
|
||||||
private java.time.LocalDateTime lastSeen; // Dernière fois que l'utilisateur était en ligne
|
private java.time.LocalDateTime lastSeen; // Dernière fois que l'utilisateur était en ligne
|
||||||
|
|
||||||
|
/** true = compte actif ; false = manager suspendu (abonnement expiré / paiement échoué). Par défaut true. */
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean isActive = true;
|
||||||
|
|
||||||
// Utilisation de BCrypt pour hacher les mots de passe de manière sécurisée
|
// Utilisation de BCrypt pour hacher les mots de passe de manière sécurisée
|
||||||
// private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
// private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.lions.dev.repository;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.booking.Booking;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BookingRepository implements PanacheRepositoryBase<Booking, UUID> {
|
||||||
|
|
||||||
|
public List<Booking> findByUserId(UUID userId) {
|
||||||
|
return list("user.id", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.lions.dev.repository;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.establishment.EstablishmentPayment;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class EstablishmentPaymentRepository implements PanacheRepositoryBase<EstablishmentPayment, UUID> {
|
||||||
|
|
||||||
|
public Optional<EstablishmentPayment> findByWaveSessionId(String waveSessionId) {
|
||||||
|
return find("waveSessionId", waveSessionId).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EstablishmentPayment> findByEstablishmentId(UUID establishmentId) {
|
||||||
|
return list("establishmentId", establishmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,5 +84,16 @@ public class EstablishmentRepository implements PanacheRepositoryBase<Establishm
|
|||||||
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
LOG.info("[LOG] Nombre d'établissements trouvés : " + establishments.size());
|
||||||
return establishments;
|
return establishments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte le nombre d'établissements gérés par un responsable.
|
||||||
|
* Contrainte métier : un Manager ne peut gérer qu'un seul établissement.
|
||||||
|
*
|
||||||
|
* @param managerId L'ID du responsable.
|
||||||
|
* @return Le nombre d'établissements (0 ou 1).
|
||||||
|
*/
|
||||||
|
public long countByManagerId(UUID managerId) {
|
||||||
|
return count("manager.id = ?1", managerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.lions.dev.repository;
|
||||||
|
|
||||||
|
import com.lions.dev.entity.establishment.EstablishmentSubscription;
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class EstablishmentSubscriptionRepository implements PanacheRepositoryBase<EstablishmentSubscription, UUID> {
|
||||||
|
|
||||||
|
public Optional<EstablishmentSubscription> findByWaveSessionId(String waveSessionId) {
|
||||||
|
return find("waveSessionId", waveSessionId).firstResultOptional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EstablishmentSubscription> findByEstablishmentId(UUID establishmentId) {
|
||||||
|
return list("establishmentId", establishmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<EstablishmentSubscription> findActiveByEstablishmentId(UUID establishmentId) {
|
||||||
|
return find("establishmentId = ?1 and status = ?2", establishmentId, EstablishmentSubscription.STATUS_ACTIVE)
|
||||||
|
.firstResultOptional();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,4 +80,24 @@ public class EventsRepository implements PanacheRepositoryBase<Events, UUID> {
|
|||||||
LOG.info("[LOG] " + events.size() + " événement(s) récupéré(s) des amis");
|
LOG.info("[LOG] " + events.size() + " événement(s) récupéré(s) des amis");
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les événements dont l'établissement correspond à la localisation (ville ou adresse).
|
||||||
|
* v2.0 : la colonne location a été supprimée ; on filtre par establishment.address ou establishment.city.
|
||||||
|
*
|
||||||
|
* @param location Fragment de ville ou d'adresse (recherche partielle, insensible à la casse).
|
||||||
|
* @return Liste des événements dont l'établissement matche la localisation.
|
||||||
|
*/
|
||||||
|
public List<Events> findEventsByEstablishmentLocation(String location) {
|
||||||
|
if (location == null || location.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
String pattern = "%" + location.trim().toLowerCase() + "%";
|
||||||
|
return getEntityManager()
|
||||||
|
.createQuery(
|
||||||
|
"SELECT e FROM Events e JOIN e.establishment est WHERE est IS NOT NULL AND (LOWER(est.address) LIKE :loc OR LOWER(est.city) LIKE :loc)",
|
||||||
|
Events.class)
|
||||||
|
.setParameter("loc", pattern)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ public class FriendshipRepository implements PanacheRepositoryBase<Friendship, U
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Trouver une relation d'amitié entre deux utilisateurs spécifiés.
|
* Trouver une relation d'amitié entre deux utilisateurs spécifiés.
|
||||||
* Cette méthode recherche une relation d'amitié entre deux utilisateurs donnés.
|
* Cette méthode recherche une relation d'amitié entre deux utilisateurs donnés,
|
||||||
* Elle peut être utilisée pour vérifier si une demande d'amitié existe déjà.
|
* dans les deux sens (user→friend ou friend→user) car une demande peut exister
|
||||||
|
* dans l'une ou l'autre direction.
|
||||||
*
|
*
|
||||||
* @param user L'utilisateur qui envoie la demande d'amitié.
|
* @param user L'utilisateur qui envoie la demande d'amitié.
|
||||||
* @param friend L'ami qui reçoit la demande.
|
* @param friend L'ami qui reçoit la demande.
|
||||||
@@ -36,8 +37,8 @@ public class FriendshipRepository implements PanacheRepositoryBase<Friendship, U
|
|||||||
public Optional<Friendship> findByUsers(Users user, Users friend) {
|
public Optional<Friendship> findByUsers(Users user, Users friend) {
|
||||||
logger.infof("Recherche de la relation d'amitié entre les utilisateurs : %s et %s", user.getId(), friend.getId());
|
logger.infof("Recherche de la relation d'amitié entre les utilisateurs : %s et %s", user.getId(), friend.getId());
|
||||||
|
|
||||||
// Requête qui cherche une relation d'amitié entre deux utilisateurs spécifiques
|
// Vérifier dans les deux sens : (user, friend) ET (friend, user)
|
||||||
Optional<Friendship> friendship = find("user = ?1 and friend = ?2", user, friend).firstResultOptional();
|
Optional<Friendship> friendship = find("(user = ?1 AND friend = ?2) OR (user = ?2 AND friend = ?1)", user, friend).firstResultOptional();
|
||||||
|
|
||||||
if (friendship.isPresent()) {
|
if (friendship.isPresent()) {
|
||||||
logger.infof("Relation d'amitié trouvée entre %s et %s", user.getId(), friend.getId());
|
logger.infof("Relation d'amitié trouvée entre %s et %s", user.getId(), friend.getId());
|
||||||
|
|||||||
69
src/main/java/com/lions/dev/resource/AdminStatsResource.java
Normal file
69
src/main/java/com/lions/dev/resource/AdminStatsResource.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import com.lions.dev.dto.response.admin.AdminRevenueResponseDTO;
|
||||||
|
import com.lions.dev.dto.response.admin.ManagerStatsResponseDTO;
|
||||||
|
import com.lions.dev.service.AdminStatsService;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ressource REST pour le tableau de bord Super Admin.
|
||||||
|
* Requiert le header X-Super-Admin-Key pour toutes les opérations.
|
||||||
|
*/
|
||||||
|
@Path("/admin/stats")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = "Admin Stats", description = "Statistiques et KPIs (Super Admin)")
|
||||||
|
public class AdminStatsResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(AdminStatsResource.class);
|
||||||
|
private static final String SUPER_ADMIN_KEY_HEADER = "X-Super-Admin-Key";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AdminStatsService adminStatsService;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.super-admin.api-key", defaultValue = "")
|
||||||
|
Optional<String> superAdminApiKey;
|
||||||
|
|
||||||
|
private boolean isAuthorized(String apiKeyHeader) {
|
||||||
|
if (superAdminApiKey == null || !superAdminApiKey.isPresent() || superAdminApiKey.get().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return apiKeyHeader != null && apiKeyHeader.equals(superAdminApiKey.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/revenue")
|
||||||
|
@Operation(summary = "Revenus des abonnements", description = "Total des abonnements actifs * prix. Réservé Super Admin.")
|
||||||
|
public Response getRevenue(@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||||
|
if (!isAuthorized(apiKeyHeader)) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("{\"message\": \"Clé Super Admin invalide ou absente.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
AdminRevenueResponseDTO dto = adminStatsService.getRevenue();
|
||||||
|
return Response.ok(dto).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/managers")
|
||||||
|
@Operation(summary = "Liste des managers", description = "Managers avec statut (Actif/Suspendu) et date d'expiration. Réservé Super Admin.")
|
||||||
|
public Response getManagers(@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||||
|
if (!isAuthorized(apiKeyHeader)) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("{\"message\": \"Clé Super Admin invalide ou absente.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
List<ManagerStatsResponseDTO> list = adminStatsService.getManagers();
|
||||||
|
return Response.ok(list).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/main/java/com/lions/dev/resource/BookingResource.java
Normal file
100
src/main/java/com/lions/dev/resource/BookingResource.java
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import com.lions.dev.dto.request.booking.ReservationCreateRequestDTO;
|
||||||
|
import com.lions.dev.dto.response.booking.ReservationResponseDTO;
|
||||||
|
import com.lions.dev.service.BookingService;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ressource REST pour les réservations (bookings).
|
||||||
|
* Path /reservations pour alignement avec le frontend Flutter (ReservationsScreen).
|
||||||
|
*/
|
||||||
|
@Path("/reservations")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = "Reservations", description = "Réservations d'établissements")
|
||||||
|
public class BookingResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(BookingResource.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BookingService bookingService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/user/{userId}")
|
||||||
|
@Operation(summary = "Liste des réservations d'un utilisateur")
|
||||||
|
public Response getUserReservations(@PathParam("userId") UUID userId) {
|
||||||
|
List<ReservationResponseDTO> list = bookingService.getReservationsByUserId(userId);
|
||||||
|
return Response.ok(list).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{id}")
|
||||||
|
@Operation(summary = "Détail d'une réservation")
|
||||||
|
public Response getReservation(@PathParam("id") UUID id) {
|
||||||
|
ReservationResponseDTO dto = bookingService.getReservationById(id);
|
||||||
|
if (dto == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
return Response.ok(dto).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Transactional
|
||||||
|
@Operation(summary = "Créer une réservation")
|
||||||
|
public Response createReservation(@Valid ReservationCreateRequestDTO dto) {
|
||||||
|
try {
|
||||||
|
ReservationResponseDTO created = bookingService.createReservation(dto);
|
||||||
|
return Response.status(Response.Status.CREATED).entity(created).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/{id}")
|
||||||
|
@Transactional
|
||||||
|
@Operation(summary = "Mettre à jour une réservation")
|
||||||
|
public Response updateReservation(@PathParam("id") UUID id, @Valid ReservationCreateRequestDTO dto) {
|
||||||
|
ReservationResponseDTO updated = bookingService.updateReservation(id, dto);
|
||||||
|
if (updated == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
return Response.ok(updated).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/{id}/cancel")
|
||||||
|
@Transactional
|
||||||
|
@Operation(summary = "Annuler une réservation")
|
||||||
|
public Response cancelReservation(@PathParam("id") UUID id) {
|
||||||
|
ReservationResponseDTO dto = bookingService.cancelReservation(id);
|
||||||
|
if (dto == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
return Response.ok(dto).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/{id}")
|
||||||
|
@Transactional
|
||||||
|
@Operation(summary = "Supprimer une réservation")
|
||||||
|
public Response deleteReservation(@PathParam("id") UUID id) {
|
||||||
|
if (bookingService.getReservationById(id) == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
bookingService.deleteReservation(id);
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,10 @@ import com.lions.dev.dto.request.establishment.EstablishmentUpdateRequestDTO;
|
|||||||
import com.lions.dev.dto.response.establishment.EstablishmentResponseDTO;
|
import com.lions.dev.dto.response.establishment.EstablishmentResponseDTO;
|
||||||
import com.lions.dev.entity.establishment.Establishment;
|
import com.lions.dev.entity.establishment.Establishment;
|
||||||
import com.lions.dev.entity.users.Users;
|
import com.lions.dev.entity.users.Users;
|
||||||
|
import com.lions.dev.repository.EstablishmentRepository;
|
||||||
import com.lions.dev.repository.UsersRepository;
|
import com.lions.dev.repository.UsersRepository;
|
||||||
import com.lions.dev.service.EstablishmentService;
|
import com.lions.dev.service.EstablishmentService;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -38,6 +40,9 @@ public class EstablishmentResource {
|
|||||||
@Inject
|
@Inject
|
||||||
UsersRepository usersRepository;
|
UsersRepository usersRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EstablishmentRepository establishmentRepository;
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(EstablishmentResource.class);
|
private static final Logger LOG = Logger.getLogger(EstablishmentResource.class);
|
||||||
|
|
||||||
// *********** Création d'un établissement ***********
|
// *********** Création d'un établissement ***********
|
||||||
@@ -67,6 +72,26 @@ public class EstablishmentResource {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérification rôle : seuls les Managers (ou ADMIN) peuvent créer un établissement
|
||||||
|
String role = manager.getRole() != null ? manager.getRole().toUpperCase() : "";
|
||||||
|
if (!UserRoles.MANAGER.equals(role) && !UserRoles.ADMIN.equals(role) && !UserRoles.SUPER_ADMIN.equals(role)) {
|
||||||
|
LOG.error("[ERROR] L'utilisateur " + requestDTO.getManagerId() + " n'a pas le rôle Manager. Rôle : " + role);
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("Seuls les Managers peuvent créer des établissements")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrainte : un Manager ne peut gérer qu'un seul établissement (ADMIN/SUPER_ADMIN exclus)
|
||||||
|
if (UserRoles.MANAGER.equals(role)) {
|
||||||
|
long count = establishmentRepository.countByManagerId(requestDTO.getManagerId());
|
||||||
|
if (count >= 1) {
|
||||||
|
LOG.error("[ERROR] Le Manager " + requestDTO.getManagerId() + " possède déjà un établissement");
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("Un Manager ne peut gérer qu'un seul établissement")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Créer l'établissement
|
// Créer l'établissement
|
||||||
Establishment establishment = new Establishment();
|
Establishment establishment = new Establishment();
|
||||||
establishment.setName(requestDTO.getName());
|
establishment.setName(requestDTO.getName());
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import com.lions.dev.dto.request.establishment.InitiateSubscriptionRequestDTO;
|
||||||
|
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
|
||||||
|
import com.lions.dev.service.WavePaymentService;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ressource pour les abonnements / droits d'accès des établissements (paiement Wave).
|
||||||
|
*/
|
||||||
|
@Path("/establishments/{establishmentId}/subscriptions")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = "Establishment Subscriptions", description = "Paiement des droits d'accès via Wave")
|
||||||
|
public class EstablishmentSubscriptionResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(EstablishmentSubscriptionResource.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
WavePaymentService wavePaymentService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initie un paiement Wave pour les droits d'accès d'un établissement.
|
||||||
|
* Retourne l'URL de redirection vers la page de paiement Wave.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/initiate")
|
||||||
|
@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.")
|
||||||
|
public Response initiatePayment(
|
||||||
|
@PathParam("establishmentId") UUID establishmentId,
|
||||||
|
@Valid InitiateSubscriptionRequestDTO request) {
|
||||||
|
try {
|
||||||
|
InitiateSubscriptionResponseDTO response = wavePaymentService.initiatePayment(
|
||||||
|
establishmentId,
|
||||||
|
request.getPlan(),
|
||||||
|
request.getClientPhone()
|
||||||
|
);
|
||||||
|
return Response.ok(response).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
LOG.warn(e.getMessage());
|
||||||
|
return Response.status(Response.Status.NOT_FOUND).entity("{\"message\": \"" + e.getMessage() + "\"}").build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'établissement a un abonnement actif.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/status")
|
||||||
|
@Operation(summary = "Statut d'abonnement", description = "Indique si l'établissement a des droits d'accès actifs.")
|
||||||
|
public Response getSubscriptionStatus(@PathParam("establishmentId") UUID establishmentId) {
|
||||||
|
boolean active = wavePaymentService.hasActiveSubscription(establishmentId);
|
||||||
|
return Response.ok(Map.of("hasActiveSubscription", active)).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import com.lions.dev.dto.response.friends.FriendshipReadStatusResponseDTO;
|
|||||||
import com.lions.dev.entity.friends.FriendshipStatus;
|
import com.lions.dev.entity.friends.FriendshipStatus;
|
||||||
import com.lions.dev.exception.UserNotFoundException;
|
import com.lions.dev.exception.UserNotFoundException;
|
||||||
import com.lions.dev.service.FriendshipService;
|
import com.lions.dev.service.FriendshipService;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
@@ -82,6 +83,16 @@ public class FriendshipResource {
|
|||||||
+ " et "
|
+ " et "
|
||||||
+ friendshipResponse.getFriendId());
|
+ friendshipResponse.getFriendId());
|
||||||
return Response.ok(friendshipResponse).build();
|
return Response.ok(friendshipResponse).build();
|
||||||
|
} catch (UserNotFoundException e) {
|
||||||
|
logger.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||||
|
.build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.warn("[WARN] Requête invalide : " + e.getMessage());
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity("{\"message\": \"" + e.getMessage() + "\"}")
|
||||||
|
.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("[ERROR] Erreur lors de l'envoi de la demande d'amitié : " + e.getMessage(), e);
|
logger.error("[ERROR] Erreur lors de l'envoi de la demande d'amitié : " + e.getMessage(), e);
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.lions.dev.resource;
|
|||||||
import com.lions.dev.dto.request.social.SocialPostCreateRequestDTO;
|
import com.lions.dev.dto.request.social.SocialPostCreateRequestDTO;
|
||||||
import com.lions.dev.dto.response.social.SocialPostResponseDTO;
|
import com.lions.dev.dto.response.social.SocialPostResponseDTO;
|
||||||
import com.lions.dev.entity.social.SocialPost;
|
import com.lions.dev.entity.social.SocialPost;
|
||||||
|
import com.lions.dev.exception.UserNotFoundException;
|
||||||
import com.lions.dev.service.SocialPostService;
|
import com.lions.dev.service.SocialPostService;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
@@ -121,6 +122,11 @@ public class SocialPostResource {
|
|||||||
);
|
);
|
||||||
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
|
SocialPostResponseDTO responseDTO = new SocialPostResponseDTO(post);
|
||||||
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
|
return Response.status(Response.Status.CREATED).entity(responseDTO).build();
|
||||||
|
} catch (UserNotFoundException e) {
|
||||||
|
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||||
|
.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("[ERROR] Erreur lors de la création du post : " + e.getMessage(), e);
|
LOG.error("[ERROR] Erreur lors de la création du post : " + e.getMessage(), e);
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
@@ -382,6 +388,11 @@ public class SocialPostResource {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return Response.ok(responseDTOs).build();
|
return Response.ok(responseDTOs).build();
|
||||||
|
} catch (UserNotFoundException e) {
|
||||||
|
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||||
|
.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("[ERROR] Erreur lors de la récupération des posts : " + e.getMessage(), e);
|
LOG.error("[ERROR] Erreur lors de la récupération des posts : " + e.getMessage(), e);
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
@@ -416,6 +427,11 @@ public class SocialPostResource {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return Response.ok(responseDTOs).build();
|
return Response.ok(responseDTOs).build();
|
||||||
|
} catch (UserNotFoundException e) {
|
||||||
|
LOG.warn("[WARN] Utilisateur non trouvé : " + e.getMessage());
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||||
|
.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("[ERROR] Erreur lors de la récupération des posts des amis : " + e.getMessage(), e);
|
LOG.error("[ERROR] Erreur lors de la récupération des posts des amis : " + e.getMessage(), e);
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.lions.dev.resource;
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
import com.lions.dev.dto.PasswordResetRequest;
|
import com.lions.dev.dto.PasswordResetRequest;
|
||||||
|
import com.lions.dev.dto.request.users.AssignRoleRequestDTO;
|
||||||
import com.lions.dev.dto.request.users.UserAuthenticateRequestDTO;
|
import com.lions.dev.dto.request.users.UserAuthenticateRequestDTO;
|
||||||
import com.lions.dev.dto.request.users.UserCreateRequestDTO;
|
import com.lions.dev.dto.request.users.UserCreateRequestDTO;
|
||||||
import com.lions.dev.dto.response.users.UserAuthenticateResponseDTO;
|
import com.lions.dev.dto.response.users.UserAuthenticateResponseDTO;
|
||||||
@@ -16,9 +17,11 @@ import jakarta.validation.constraints.NotNull;
|
|||||||
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.config.inject.ConfigProperty;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||||
@@ -39,8 +42,13 @@ public class UsersResource {
|
|||||||
@Inject
|
@Inject
|
||||||
UsersService userService;
|
UsersService userService;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "afterwork.super-admin.api-key", defaultValue = "")
|
||||||
|
Optional<String> superAdminApiKey;
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(UsersResource.class);
|
private static final Logger LOG = Logger.getLogger(UsersResource.class);
|
||||||
|
|
||||||
|
private static final String SUPER_ADMIN_KEY_HEADER = "X-Super-Admin-Key";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Endpoint pour créer un nouvel utilisateur.
|
* Endpoint pour créer un nouvel utilisateur.
|
||||||
*
|
*
|
||||||
@@ -296,11 +304,11 @@ public class UsersResource {
|
|||||||
Users user = userService.findByEmail(request.getEmail());
|
Users user = userService.findByEmail(request.getEmail());
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
// TODO: Generer un token de reset et l'envoyer par email
|
// En standby : pas encore de service d'envoi de mail. Quand disponible :
|
||||||
// Pour l'instant, on retourne success pour ne pas reveler si l'email existe
|
// - generer un token de reset (table dédiée ou champ user avec expiration)
|
||||||
// String resetToken = generateResetToken();
|
// - appeler emailService.sendPasswordResetEmail(user.getEmail(), resetToken)
|
||||||
// emailService.sendPasswordResetEmail(user.getEmail(), resetToken);
|
// Pour l'instant, on retourne success pour ne pas reveler si l'email existe.
|
||||||
LOG.info("Utilisateur trouve, email de reinitialisation devrait etre envoye : " + request.getEmail());
|
LOG.info("Utilisateur trouve, email de reinitialisation (en standby - pas de mail service) : " + request.getEmail());
|
||||||
} else {
|
} else {
|
||||||
LOG.info("Aucun utilisateur trouve avec cet email (ne pas reveler) : " + request.getEmail());
|
LOG.info("Aucun utilisateur trouve avec cet email (ne pas reveler) : " + request.getEmail());
|
||||||
}
|
}
|
||||||
@@ -318,4 +326,98 @@ public class UsersResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribue un rôle à un utilisateur (réservé au super administrateur).
|
||||||
|
* Requiert le header X-Super-Admin-Key correspondant à afterwork.super-admin.api-key.
|
||||||
|
*
|
||||||
|
* @param id L'ID de l'utilisateur.
|
||||||
|
* @param request Le DTO contenant le nouveau rôle.
|
||||||
|
* @param apiKeyHeader Valeur du header X-Super-Admin-Key (injecté via @HeaderParam).
|
||||||
|
* @return L'utilisateur mis à jour.
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Path("/{id}/role")
|
||||||
|
@Transactional
|
||||||
|
@Operation(summary = "Attribuer un rôle à un utilisateur (super admin)",
|
||||||
|
description = "Modifie le rôle d'un utilisateur. Réservé au super administrateur (header X-Super-Admin-Key).")
|
||||||
|
@APIResponse(responseCode = "200", description = "Rôle mis à jour")
|
||||||
|
@APIResponse(responseCode = "403", description = "Clé super admin invalide ou absente")
|
||||||
|
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé")
|
||||||
|
public Response assignRole(
|
||||||
|
@PathParam("id") UUID id,
|
||||||
|
@Valid AssignRoleRequestDTO request,
|
||||||
|
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||||
|
|
||||||
|
String key = superAdminApiKey.orElse("");
|
||||||
|
if (key.isBlank()) {
|
||||||
|
LOG.warn("Opération assignRole refusée : afterwork.super-admin.api-key non configurée");
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("{\"message\": \"Opération non autorisée : clé super admin non configurée.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
if (apiKeyHeader == null || !apiKeyHeader.equals(key)) {
|
||||||
|
LOG.warn("Opération assignRole refusée : clé super admin invalide ou absente");
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("{\"message\": \"Clé super administrateur invalide ou absente.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Users user = userService.assignRole(id, request.getRole());
|
||||||
|
UserCreateResponseDTO responseDTO = new UserCreateResponseDTO(user);
|
||||||
|
return Response.ok(responseDTO).build();
|
||||||
|
} catch (UserNotFoundException e) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("{\"message\": \"" + e.getMessage() + "\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un token temporaire d'impersonation (Super Admin se connecte en tant qu'un autre utilisateur).
|
||||||
|
* Le client envoie ce token (ex: Authorization: Bearer <token>) pour les requêtes suivantes.
|
||||||
|
* La validation du token côté backend (filter) est à implémenter si nécessaire.
|
||||||
|
*
|
||||||
|
* @param id L'ID de l'utilisateur à impersonner.
|
||||||
|
* @param apiKeyHeader X-Super-Admin-Key.
|
||||||
|
* @return JSON avec impersonationToken, expiresInSeconds, userId.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/{id}/impersonate")
|
||||||
|
@Operation(summary = "Impersonation (Super Admin)",
|
||||||
|
description = "Génère un token temporaire pour se connecter en tant que cet utilisateur. Header X-Super-Admin-Key requis.")
|
||||||
|
public Response impersonate(
|
||||||
|
@PathParam("id") UUID id,
|
||||||
|
@HeaderParam(SUPER_ADMIN_KEY_HEADER) String apiKeyHeader) {
|
||||||
|
|
||||||
|
String key = superAdminApiKey.orElse("");
|
||||||
|
if (key.isBlank()) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("{\"message\": \"Opération non autorisée : clé super admin non configurée.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
if (apiKeyHeader == null || !apiKeyHeader.equals(key)) {
|
||||||
|
return Response.status(Response.Status.FORBIDDEN)
|
||||||
|
.entity("{\"message\": \"Clé super administrateur invalide ou absente.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Users user = userService.getUserById(id);
|
||||||
|
// Token temporaire (UUID) — à valider côté backend via un filter si besoin
|
||||||
|
String token = UUID.randomUUID().toString();
|
||||||
|
int expiresInSeconds = 900; // 15 min
|
||||||
|
return Response.ok(Map.of(
|
||||||
|
"impersonationToken", token,
|
||||||
|
"expiresInSeconds", expiresInSeconds,
|
||||||
|
"userId", user.getId().toString(),
|
||||||
|
"email", user.getEmail()
|
||||||
|
)).build();
|
||||||
|
} catch (UserNotFoundException e) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("{\"message\": \"Utilisateur non trouvé.\"}")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.lions.dev.resource;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.lions.dev.service.WavePaymentService;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook Wave pour les notifications de paiement (payment.completed, payment.cancelled, etc.).
|
||||||
|
*/
|
||||||
|
@Path("/webhooks/wave")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public class WaveWebhookResource {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(WaveWebhookResource.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
WavePaymentService wavePaymentService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
public Response handleWebhook(String payload) {
|
||||||
|
try {
|
||||||
|
JsonNode node = objectMapper.readTree(payload);
|
||||||
|
wavePaymentService.handleWebhook(node);
|
||||||
|
return Response.ok().build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Erreur traitement webhook Wave", e);
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/main/java/com/lions/dev/service/AdminStatsService.java
Normal file
78
src/main/java/com/lions/dev/service/AdminStatsService.java
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package com.lions.dev.service;
|
||||||
|
|
||||||
|
import com.lions.dev.dto.response.admin.AdminRevenueResponseDTO;
|
||||||
|
import com.lions.dev.dto.response.admin.ManagerStatsResponseDTO;
|
||||||
|
import com.lions.dev.entity.establishment.Establishment;
|
||||||
|
import com.lions.dev.entity.establishment.EstablishmentSubscription;
|
||||||
|
import com.lions.dev.entity.users.Users;
|
||||||
|
import com.lions.dev.repository.EstablishmentRepository;
|
||||||
|
import com.lions.dev.repository.EstablishmentSubscriptionRepository;
|
||||||
|
import com.lions.dev.repository.UsersRepository;
|
||||||
|
import com.lions.dev.util.UserRoles;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AdminStatsService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EstablishmentSubscriptionRepository subscriptionRepository;
|
||||||
|
@Inject
|
||||||
|
EstablishmentRepository establishmentRepository;
|
||||||
|
@Inject
|
||||||
|
UsersRepository usersRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revenus totaux : somme des montants des abonnements actifs.
|
||||||
|
*/
|
||||||
|
public AdminRevenueResponseDTO getRevenue() {
|
||||||
|
List<EstablishmentSubscription> active = subscriptionRepository.list(
|
||||||
|
"status", EstablishmentSubscription.STATUS_ACTIVE);
|
||||||
|
long count = active.size();
|
||||||
|
BigDecimal total = active.stream()
|
||||||
|
.map(s -> s.getAmountXof() != null ? BigDecimal.valueOf(s.getAmountXof()) : BigDecimal.ZERO)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
return new AdminRevenueResponseDTO(total, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des managers avec statut (Actif/Suspendu) et date d'expiration d'abonnement.
|
||||||
|
*/
|
||||||
|
public List<ManagerStatsResponseDTO> getManagers() {
|
||||||
|
List<Users> managers = usersRepository.list("role", UserRoles.MANAGER);
|
||||||
|
List<ManagerStatsResponseDTO> result = new ArrayList<>();
|
||||||
|
for (Users manager : managers) {
|
||||||
|
List<Establishment> establishments = establishmentRepository.findByManagerId(manager.getId());
|
||||||
|
UUID establishmentId = null;
|
||||||
|
String establishmentName = null;
|
||||||
|
java.time.LocalDateTime expiresAt = null;
|
||||||
|
String status = manager.isActive() ? "ACTIVE" : "SUSPENDED";
|
||||||
|
if (!establishments.isEmpty()) {
|
||||||
|
Establishment est = establishments.get(0);
|
||||||
|
establishmentId = est.getId();
|
||||||
|
establishmentName = est.getName();
|
||||||
|
Optional<EstablishmentSubscription> sub = subscriptionRepository.findActiveByEstablishmentId(est.getId());
|
||||||
|
if (sub.isPresent()) {
|
||||||
|
expiresAt = sub.get().getExpiresAt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.add(new ManagerStatsResponseDTO(
|
||||||
|
manager.getId(),
|
||||||
|
manager.getEmail(),
|
||||||
|
manager.getFirstName(),
|
||||||
|
manager.getLastName(),
|
||||||
|
status,
|
||||||
|
expiresAt,
|
||||||
|
establishmentId,
|
||||||
|
establishmentName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/main/java/com/lions/dev/service/BookingService.java
Normal file
108
src/main/java/com/lions/dev/service/BookingService.java
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package com.lions.dev.service;
|
||||||
|
|
||||||
|
import com.lions.dev.dto.request.booking.ReservationCreateRequestDTO;
|
||||||
|
import com.lions.dev.dto.response.booking.ReservationResponseDTO;
|
||||||
|
import com.lions.dev.entity.booking.Booking;
|
||||||
|
import com.lions.dev.entity.establishment.Establishment;
|
||||||
|
import com.lions.dev.entity.users.Users;
|
||||||
|
import com.lions.dev.repository.BookingRepository;
|
||||||
|
import com.lions.dev.repository.EstablishmentRepository;
|
||||||
|
import com.lions.dev.repository.UsersRepository;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class BookingService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BookingRepository bookingRepository;
|
||||||
|
@Inject
|
||||||
|
EstablishmentRepository establishmentRepository;
|
||||||
|
@Inject
|
||||||
|
UsersRepository usersRepository;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_DATE_TIME;
|
||||||
|
|
||||||
|
public List<ReservationResponseDTO> getReservationsByUserId(UUID userId) {
|
||||||
|
return bookingRepository.findByUserId(userId).stream()
|
||||||
|
.map(ReservationResponseDTO::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReservationResponseDTO getReservationById(UUID id) {
|
||||||
|
Booking booking = bookingRepository.findById(id);
|
||||||
|
if (booking == null) return null;
|
||||||
|
return new ReservationResponseDTO(booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ReservationResponseDTO createReservation(ReservationCreateRequestDTO dto) {
|
||||||
|
Establishment establishment = establishmentRepository.findById(dto.getEstablishmentId());
|
||||||
|
Users user = usersRepository.findById(dto.getUserId());
|
||||||
|
if (establishment == null || user == null) {
|
||||||
|
throw new IllegalArgumentException("Établissement ou utilisateur non trouvé");
|
||||||
|
}
|
||||||
|
LocalDateTime reservationTime = parseReservationDate(dto.getReservationDate());
|
||||||
|
Booking booking = new Booking();
|
||||||
|
booking.setEstablishment(establishment);
|
||||||
|
booking.setUser(user);
|
||||||
|
booking.setReservationTime(reservationTime);
|
||||||
|
booking.setGuestCount(dto.getNumberOfPeople() > 0 ? dto.getNumberOfPeople() : 1);
|
||||||
|
booking.setStatus("PENDING");
|
||||||
|
booking.setSpecialRequests(dto.getNotes());
|
||||||
|
bookingRepository.persist(booking);
|
||||||
|
return new ReservationResponseDTO(booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ReservationResponseDTO cancelReservation(UUID id) {
|
||||||
|
Booking booking = bookingRepository.findById(id);
|
||||||
|
if (booking == null) return null;
|
||||||
|
booking.setStatus("CANCELLED");
|
||||||
|
bookingRepository.persist(booking);
|
||||||
|
return new ReservationResponseDTO(booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteReservation(UUID id) {
|
||||||
|
bookingRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ReservationResponseDTO updateReservation(UUID id, ReservationCreateRequestDTO dto) {
|
||||||
|
Booking booking = bookingRepository.findById(id);
|
||||||
|
if (booking == null) return null;
|
||||||
|
if (dto.getReservationDate() != null) {
|
||||||
|
booking.setReservationTime(parseReservationDate(dto.getReservationDate()));
|
||||||
|
}
|
||||||
|
if (dto.getNumberOfPeople() > 0) {
|
||||||
|
booking.setGuestCount(dto.getNumberOfPeople());
|
||||||
|
}
|
||||||
|
if (dto.getNotes() != null) {
|
||||||
|
booking.setSpecialRequests(dto.getNotes());
|
||||||
|
}
|
||||||
|
bookingRepository.persist(booking);
|
||||||
|
return new ReservationResponseDTO(booking);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDateTime parseReservationDate(String value) {
|
||||||
|
if (value == null || value.isBlank()) return LocalDateTime.now();
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(value, ISO);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
int len = Math.min(19, value.length());
|
||||||
|
return LocalDateTime.parse(value.substring(0, len));
|
||||||
|
} catch (Exception e2) {
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,7 @@ public class EstablishmentService {
|
|||||||
"SELECT DISTINCT e FROM Establishment e " +
|
"SELECT DISTINCT e FROM Establishment e " +
|
||||||
"LEFT JOIN FETCH e.medias m " +
|
"LEFT JOIN FETCH e.medias m " +
|
||||||
"LEFT JOIN FETCH e.manager " +
|
"LEFT JOIN FETCH e.manager " +
|
||||||
|
"WHERE (e.isActive IS NULL OR e.isActive = true) " +
|
||||||
"ORDER BY e.name ASC",
|
"ORDER BY e.name ASC",
|
||||||
Establishment.class
|
Establishment.class
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public class EventService {
|
|||||||
notificationEmitter.send(kafkaEvent);
|
notificationEmitter.send(kafkaEvent);
|
||||||
logger.debug("[logger] Événement event_created publié dans Kafka pour: {}", friend.getId());
|
logger.debug("[logger] Événement event_created publié dans Kafka pour: {}", friend.getId());
|
||||||
} catch (Exception kafkaEx) {
|
} catch (Exception kafkaEx) {
|
||||||
logger.error("[ERROR] Erreur publication Kafka: {}", kafkaEx.getMessage());
|
logger.error("[ERROR] Erreur publication Kafka pour événement {}", event.getId(), kafkaEx);
|
||||||
// Ne pas bloquer si Kafka échoue
|
// Ne pas bloquer si Kafka échoue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,14 +424,15 @@ public class EventService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les événements par localisation.
|
* Récupère les événements par localisation (ville ou adresse de l'établissement).
|
||||||
|
* v2.0 : plus de colonne location ; recherche sur establishment.address et establishment.city.
|
||||||
*
|
*
|
||||||
* @param location La localisation des événements.
|
* @param location Fragment de localisation (ville ou adresse).
|
||||||
* @return La liste des événements situés à cette localisation.
|
* @return La liste des événements dont l'établissement matche.
|
||||||
*/
|
*/
|
||||||
public List<Events> findEventsByLocation(String location) {
|
public List<Events> findEventsByLocation(String location) {
|
||||||
logger.info("[logger] Récupération des événements pour la localisation : " + location);
|
logger.info("[logger] Récupération des événements pour la localisation : " + location);
|
||||||
List<Events> events = eventsRepository.find("location", location).list();
|
List<Events> events = eventsRepository.findEventsByEstablishmentLocation(location);
|
||||||
logger.info("[logger] Nombre d'événements trouvés pour la localisation '" + location + "' : " + events.size());
|
logger.info("[logger] Nombre d'événements trouvés pour la localisation '" + location + "' : " + events.size());
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ public class FriendshipService {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public FriendshipCreateOneResponseDTO sendFriendRequest(FriendshipCreateOneRequestDTO request) {
|
public FriendshipCreateOneResponseDTO sendFriendRequest(FriendshipCreateOneRequestDTO request) {
|
||||||
|
if (request == null || request.getUserId() == null || request.getFriendId() == null) {
|
||||||
|
logger.error("[ERROR] Requête invalide : userId ou friendId manquant");
|
||||||
|
throw new IllegalArgumentException("L'identifiant de l'utilisateur et de l'ami sont requis.");
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("[LOG] Envoi d'une demande d'amitié de l'utilisateur " + request.getUserId() + " à l'utilisateur " + request.getFriendId());
|
logger.info("[LOG] Envoi d'une demande d'amitié de l'utilisateur " + request.getUserId() + " à l'utilisateur " + request.getFriendId());
|
||||||
|
|
||||||
// Récupérer les utilisateurs concernés
|
// Récupérer les utilisateurs concernés
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ public class MessageService {
|
|||||||
// Ne pas bloquer si la confirmation échoue
|
// Ne pas bloquer si la confirmation échoue
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage());
|
io.quarkus.logging.Log.error("[ERROR] Erreur lors de la publication dans Kafka pour message " + message.getId(), e);
|
||||||
// Ne pas bloquer l'envoi du message si Kafka échoue
|
// Ne pas bloquer l'envoi du message si Kafka échoue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,4 +263,24 @@ public class UsersService {
|
|||||||
Optional<Users> userOptional = usersRepository.findByEmail(email);
|
Optional<Users> userOptional = usersRepository.findByEmail(email);
|
||||||
return userOptional.orElse(null);
|
return userOptional.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribue un rôle à un utilisateur (réservé au super administrateur).
|
||||||
|
*
|
||||||
|
* @param userId L'ID de l'utilisateur à modifier.
|
||||||
|
* @param newRole Le nouveau rôle (SUPER_ADMIN, ADMIN, MANAGER, USER).
|
||||||
|
* @return L'utilisateur mis à jour.
|
||||||
|
* @throws UserNotFoundException Si l'utilisateur n'existe pas.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Users assignRole(UUID userId, String newRole) {
|
||||||
|
Users user = usersRepository.findById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new UserNotFoundException("Utilisateur non trouvé avec l'ID : " + userId);
|
||||||
|
}
|
||||||
|
user.setRole(newRole);
|
||||||
|
usersRepository.persist(user);
|
||||||
|
System.out.println("[LOG] Rôle attribué à " + user.getEmail() + " : " + newRole);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
216
src/main/java/com/lions/dev/service/WavePaymentService.java
Normal file
216
src/main/java/com/lions/dev/service/WavePaymentService.java
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package com.lions.dev.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.lions.dev.dto.response.establishment.InitiateSubscriptionResponseDTO;
|
||||||
|
import com.lions.dev.entity.establishment.Establishment;
|
||||||
|
import com.lions.dev.entity.establishment.EstablishmentPayment;
|
||||||
|
import com.lions.dev.entity.establishment.EstablishmentSubscription;
|
||||||
|
import com.lions.dev.entity.users.Users;
|
||||||
|
import com.lions.dev.repository.EstablishmentPaymentRepository;
|
||||||
|
import com.lions.dev.repository.EstablishmentRepository;
|
||||||
|
import com.lions.dev.repository.EstablishmentSubscriptionRepository;
|
||||||
|
import com.lions.dev.repository.UsersRepository;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service d'intégration Wave pour le paiement des droits d'accès des établissements.
|
||||||
|
* Crée une session de paiement Wave et traite les webhooks (payment.completed, etc.).
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class WavePaymentService {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(WavePaymentService.class);
|
||||||
|
|
||||||
|
private static final int MONTHLY_AMOUNT_XOF = 15_000;
|
||||||
|
private static final int YEARLY_AMOUNT_XOF = 150_000;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EstablishmentRepository establishmentRepository;
|
||||||
|
@Inject
|
||||||
|
EstablishmentSubscriptionRepository subscriptionRepository;
|
||||||
|
@Inject
|
||||||
|
EstablishmentPaymentRepository paymentRepository;
|
||||||
|
@Inject
|
||||||
|
UsersRepository usersRepository;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "wave.api.url", defaultValue = "https://api.wave.com")
|
||||||
|
String waveApiUrl;
|
||||||
|
|
||||||
|
@ConfigProperty(name = "wave.api.key", defaultValue = "")
|
||||||
|
Optional<String> waveApiKey;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initie un paiement Wave pour un établissement (droits d'accès).
|
||||||
|
* Crée une session Wave et retourne l'URL de redirection.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public InitiateSubscriptionResponseDTO initiatePayment(UUID establishmentId, String plan, String clientPhone) {
|
||||||
|
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||||
|
if (establishment == null) {
|
||||||
|
throw new IllegalArgumentException("Établissement non trouvé : " + establishmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
int amountXof = EstablishmentSubscription.PLAN_MONTHLY.equals(plan) ? MONTHLY_AMOUNT_XOF : YEARLY_AMOUNT_XOF;
|
||||||
|
String description = "AfterWork - Abonnement " + plan + " - " + establishment.getName();
|
||||||
|
|
||||||
|
EstablishmentSubscription subscription = new EstablishmentSubscription();
|
||||||
|
subscription.setEstablishmentId(establishmentId);
|
||||||
|
subscription.setPlan(plan);
|
||||||
|
subscription.setStatus(EstablishmentSubscription.STATUS_PENDING);
|
||||||
|
subscription.setAmountXof(amountXof);
|
||||||
|
subscriptionRepository.persist(subscription);
|
||||||
|
|
||||||
|
EstablishmentPayment payment = new EstablishmentPayment();
|
||||||
|
payment.setEstablishmentId(establishmentId);
|
||||||
|
payment.setAmountXof(amountXof);
|
||||||
|
payment.setStatus(EstablishmentPayment.STATUS_PENDING);
|
||||||
|
payment.setClientPhone(clientPhone);
|
||||||
|
payment.setPlan(plan);
|
||||||
|
paymentRepository.persist(payment);
|
||||||
|
|
||||||
|
String waveSessionId = null;
|
||||||
|
String paymentUrl = null;
|
||||||
|
|
||||||
|
String apiKey = waveApiKey.orElse("");
|
||||||
|
if (!apiKey.isBlank()) {
|
||||||
|
try {
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("amount", amountXof);
|
||||||
|
body.put("currency", "XOF");
|
||||||
|
body.put("description", description);
|
||||||
|
body.put("client_reference", subscription.getId().toString());
|
||||||
|
body.put("customer_phone_number", clientPhone.startsWith("+") ? clientPhone : "+" + clientPhone);
|
||||||
|
|
||||||
|
String bodyJson = objectMapper.writeValueAsString(body);
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(waveApiUrl + "/wave/api/v1/checkout/sessions"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + apiKey)
|
||||||
|
.timeout(Duration.ofSeconds(15))
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(bodyJson))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() >= 200 && response.statusCode() < 300) {
|
||||||
|
JsonNode node = objectMapper.readTree(response.body());
|
||||||
|
waveSessionId = node.has("id") ? node.get("id").asText() : null;
|
||||||
|
paymentUrl = node.has("payment_url") ? node.get("payment_url").asText() : null;
|
||||||
|
} else {
|
||||||
|
LOG.warn("Wave API error: " + response.statusCode() + " " + response.body());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Erreur appel Wave API", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.warn("Wave API key non configurée : utilisation d'une URL de test");
|
||||||
|
paymentUrl = "https://checkout.wave.com/session/test?ref=" + subscription.getId();
|
||||||
|
waveSessionId = "test-" + subscription.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.setWaveSessionId(waveSessionId);
|
||||||
|
payment.setWaveSessionId(waveSessionId);
|
||||||
|
subscriptionRepository.persist(subscription);
|
||||||
|
paymentRepository.persist(payment);
|
||||||
|
|
||||||
|
return new InitiateSubscriptionResponseDTO(
|
||||||
|
paymentUrl != null ? paymentUrl : "",
|
||||||
|
waveSessionId,
|
||||||
|
amountXof,
|
||||||
|
plan,
|
||||||
|
EstablishmentSubscription.STATUS_PENDING
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un établissement a un abonnement actif (droits d'accès payés).
|
||||||
|
*/
|
||||||
|
public boolean hasActiveSubscription(UUID establishmentId) {
|
||||||
|
return subscriptionRepository.findActiveByEstablishmentId(establishmentId).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite un webhook Wave (payment.completed, payment.cancelled, etc.).
|
||||||
|
* Met à jour l'abonnement et le paiement.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void handleWebhook(JsonNode payload) {
|
||||||
|
String eventType = payload.has("type") ? payload.get("type").asText() : null;
|
||||||
|
JsonNode data = payload.has("data") ? payload.get("data") : null;
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
String sessionId = data.has("id") ? data.get("id").asText() : (data.has("session_id") ? data.get("session_id").asText() : null);
|
||||||
|
if (sessionId == null) return;
|
||||||
|
|
||||||
|
subscriptionRepository.findByWaveSessionId(sessionId).ifPresent(sub -> {
|
||||||
|
UUID establishmentId = sub.getEstablishmentId();
|
||||||
|
Establishment establishment = establishmentRepository.findById(establishmentId);
|
||||||
|
|
||||||
|
if ("payment.completed".equals(eventType)) {
|
||||||
|
sub.setStatus(EstablishmentSubscription.STATUS_ACTIVE);
|
||||||
|
sub.setPaidAt(LocalDateTime.now());
|
||||||
|
if (EstablishmentSubscription.PLAN_MONTHLY.equals(sub.getPlan())) {
|
||||||
|
sub.setExpiresAt(LocalDateTime.now().plusMonths(1));
|
||||||
|
} else {
|
||||||
|
sub.setExpiresAt(LocalDateTime.now().plusYears(1));
|
||||||
|
}
|
||||||
|
subscriptionRepository.persist(sub);
|
||||||
|
// Activer l'établissement et le manager
|
||||||
|
if (establishment != null) {
|
||||||
|
establishment.setIsActive(true);
|
||||||
|
establishmentRepository.persist(establishment);
|
||||||
|
Users manager = establishment.getManager();
|
||||||
|
if (manager != null) {
|
||||||
|
manager.setActive(true);
|
||||||
|
usersRepository.persist(manager);
|
||||||
|
LOG.info("Webhook Wave: établissement et manager activés pour " + establishmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType)
|
||||||
|
|| "payment.failed".equals(eventType)) {
|
||||||
|
sub.setStatus(EstablishmentSubscription.STATUS_CANCELLED);
|
||||||
|
subscriptionRepository.persist(sub);
|
||||||
|
// Suspendre l'établissement et le manager
|
||||||
|
if (establishment != null) {
|
||||||
|
establishment.setIsActive(false);
|
||||||
|
establishmentRepository.persist(establishment);
|
||||||
|
Users manager = establishment.getManager();
|
||||||
|
if (manager != null) {
|
||||||
|
manager.setActive(false);
|
||||||
|
usersRepository.persist(manager);
|
||||||
|
LOG.info("Webhook Wave: établissement et manager suspendus pour " + establishmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
paymentRepository.findByWaveSessionId(sessionId).ifPresent(payment -> {
|
||||||
|
if ("payment.completed".equals(eventType)) {
|
||||||
|
payment.setStatus(EstablishmentPayment.STATUS_COMPLETED);
|
||||||
|
} else if ("payment.cancelled".equals(eventType) || "payment.expired".equals(eventType)) {
|
||||||
|
payment.setStatus(EstablishmentPayment.STATUS_CANCELLED);
|
||||||
|
}
|
||||||
|
paymentRepository.persist(payment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/main/java/com/lions/dev/util/UserRole.java
Normal file
46
src/main/java/com/lions/dev/util/UserRole.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.lions.dev.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rôles utilisateur de l'application AfterWork.
|
||||||
|
* Hiérarchie : SUPER_ADMIN > ADMIN > MANAGER > USER.
|
||||||
|
*/
|
||||||
|
public final class UserRole {
|
||||||
|
|
||||||
|
private UserRole() {}
|
||||||
|
|
||||||
|
/** Utilisateur standard (participation aux événements, profil, amis). */
|
||||||
|
public static final String USER = "USER";
|
||||||
|
|
||||||
|
/** Responsable d'établissement (gestion de son établissement). */
|
||||||
|
public static final String MANAGER = "MANAGER";
|
||||||
|
|
||||||
|
/** Administrateur (gestion des établissements, modération). */
|
||||||
|
public static final String ADMIN = "ADMIN";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Super administrateur : tous les droits (gestion des utilisateurs, attribution des rôles,
|
||||||
|
* gestion des établissements, accès aux paiements Wave, etc.).
|
||||||
|
*/
|
||||||
|
public static final String SUPER_ADMIN = "SUPER_ADMIN";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le rôle a les droits super admin (ou est super admin).
|
||||||
|
*/
|
||||||
|
public static boolean isSuperAdmin(String role) {
|
||||||
|
return SUPER_ADMIN.equals(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le rôle peut gérer les utilisateurs (attribution de rôles, etc.).
|
||||||
|
*/
|
||||||
|
public static boolean canManageUsers(String role) {
|
||||||
|
return SUPER_ADMIN.equals(role) || ADMIN.equals(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si le rôle peut gérer les établissements (vérification, modération).
|
||||||
|
*/
|
||||||
|
public static boolean canManageEstablishments(String role) {
|
||||||
|
return SUPER_ADMIN.equals(role) || ADMIN.equals(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/java/com/lions/dev/util/UserRoles.java
Normal file
36
src/main/java/com/lions/dev/util/UserRoles.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package com.lions.dev.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rôles utilisateur de l'application AfterWork.
|
||||||
|
* Hiérarchie : SUPER_ADMIN > ADMIN > MANAGER > USER.
|
||||||
|
*/
|
||||||
|
public final class UserRoles {
|
||||||
|
|
||||||
|
private UserRoles() {}
|
||||||
|
|
||||||
|
/** Super administrateur : tous les droits (gestion utilisateurs, attribution de rôles, etc.). */
|
||||||
|
public static final String SUPER_ADMIN = "SUPER_ADMIN";
|
||||||
|
|
||||||
|
/** Administrateur : gestion courante de l'application. */
|
||||||
|
public static final String ADMIN = "ADMIN";
|
||||||
|
|
||||||
|
/** Manager : gestion d'établissements, événements, etc. */
|
||||||
|
public static final String MANAGER = "MANAGER";
|
||||||
|
|
||||||
|
/** Utilisateur standard. */
|
||||||
|
public static final String USER = "USER";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indique si le rôle a les droits super administrateur (ou est SUPER_ADMIN).
|
||||||
|
*/
|
||||||
|
public static boolean isSuperAdmin(String role) {
|
||||||
|
return SUPER_ADMIN.equals(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indique si le rôle peut gérer les utilisateurs (attribution de rôles, etc.).
|
||||||
|
*/
|
||||||
|
public static boolean canManageUsers(String role) {
|
||||||
|
return SUPER_ADMIN.equals(role) || ADMIN.equals(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,25 +81,30 @@ public class ChatWebSocketNext {
|
|||||||
String userId = connection.pathParam("userId");
|
String userId = connection.pathParam("userId");
|
||||||
Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
|
Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
|
||||||
|
|
||||||
// Parser le message JSON
|
|
||||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
Map<String, Object> messageData = mapper.readValue(message, Map.class);
|
Map<String, Object> raw = mapper.readValue(message, Map.class);
|
||||||
|
String type = (String) raw.get("type");
|
||||||
String type = (String) messageData.get("type");
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> data = (Map<String, Object>) raw.get("data");
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "message":
|
case "message":
|
||||||
handleChatMessage(messageData, userId);
|
if (data != null) handleChatMessage(data, userId);
|
||||||
|
else Log.warn("[CHAT-WS-NEXT] Message sans 'data'");
|
||||||
break;
|
break;
|
||||||
case "typing":
|
case "typing":
|
||||||
handleTypingIndicator(messageData, userId);
|
if (data != null) handleTypingIndicator(data, userId);
|
||||||
break;
|
break;
|
||||||
case "read":
|
case "read":
|
||||||
handleReadReceipt(messageData, userId);
|
if (data != null) handleReadReceipt(data, userId);
|
||||||
|
else Log.warn("[CHAT-WS-NEXT] Read receipt sans 'data'");
|
||||||
|
break;
|
||||||
|
case "ping":
|
||||||
|
// Heartbeat - ignorer
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Log.warn("[CHAT-WS-NEXT] Type de message inconnu: " + type);
|
Log.warn("[CHAT-WS-NEXT] Type inconnu: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -108,16 +113,17 @@ public class ChatWebSocketNext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gère l'envoi d'un message de chat.
|
* Gère l'envoi d'un message de chat via WebSocket.
|
||||||
* Le message est traité par MessageService qui publiera dans Kafka.
|
* Note: L'envoi principal passe par REST (POST /messages). Cette méthode
|
||||||
|
* est pour compatibilité si le client envoie via WebSocket.
|
||||||
*/
|
*/
|
||||||
private void handleChatMessage(Map<String, Object> messageData, String senderId) {
|
private void handleChatMessage(Map<String, Object> data, String senderId) {
|
||||||
try {
|
try {
|
||||||
UUID senderUUID = UUID.fromString(senderId);
|
UUID senderUUID = UUID.fromString(senderId);
|
||||||
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
|
UUID recipientUUID = UUID.fromString((String) data.get("recipientId"));
|
||||||
String content = (String) messageData.get("content");
|
String content = (String) data.get("content");
|
||||||
String messageType = messageData.getOrDefault("messageType", "text").toString();
|
String messageType = data.getOrDefault("messageType", "text").toString();
|
||||||
String mediaUrl = (String) messageData.get("mediaUrl");
|
String mediaUrl = (String) data.get("mediaUrl");
|
||||||
|
|
||||||
// Enregistrer le message dans la base de données
|
// Enregistrer le message dans la base de données
|
||||||
// MessageService publiera automatiquement dans Kafka
|
// MessageService publiera automatiquement dans Kafka
|
||||||
@@ -146,13 +152,21 @@ public class ChatWebSocketNext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gère les indicateurs de frappe.
|
* Gère les indicateurs de frappe.
|
||||||
|
* data doit contenir recipientId (ID du destinataire) et isTyping.
|
||||||
*/
|
*/
|
||||||
private void handleTypingIndicator(Map<String, Object> messageData, String userId) {
|
private void handleTypingIndicator(Map<String, Object> data, String userId) {
|
||||||
try {
|
try {
|
||||||
UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId"));
|
Object recipientIdObj = data.get("recipientId");
|
||||||
boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false);
|
if (recipientIdObj == null) {
|
||||||
|
Log.warn("[CHAT-WS-NEXT] Typing sans recipientId - ignoré");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UUID recipientUUID = UUID.fromString(recipientIdObj.toString());
|
||||||
|
Object isTypingObj = data.get("isTyping");
|
||||||
|
boolean isTyping = isTypingObj instanceof Boolean ? (Boolean) isTypingObj : Boolean.parseBoolean(String.valueOf(isTypingObj));
|
||||||
|
|
||||||
String response = buildJsonMessage("typing", Map.of(
|
String response = buildJsonMessage("typing", Map.of(
|
||||||
|
"conversationId", data.getOrDefault("conversationId", ""),
|
||||||
"userId", userId,
|
"userId", userId,
|
||||||
"isTyping", isTyping
|
"isTyping", isTyping
|
||||||
));
|
));
|
||||||
@@ -168,22 +182,22 @@ public class ChatWebSocketNext {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gère les confirmations de lecture.
|
* Gère les confirmations de lecture.
|
||||||
|
* Envoie type "read" (format attendu par le client Flutter).
|
||||||
*/
|
*/
|
||||||
private void handleReadReceipt(Map<String, Object> messageData, String userId) {
|
private void handleReadReceipt(Map<String, Object> data, String userId) {
|
||||||
try {
|
try {
|
||||||
UUID messageUUID = UUID.fromString((String) messageData.get("messageId"));
|
UUID messageUUID = UUID.fromString((String) data.get("messageId"));
|
||||||
|
|
||||||
// Marquer le message comme lu
|
|
||||||
Message message = messageService.markMessageAsRead(messageUUID);
|
Message message = messageService.markMessageAsRead(messageUUID);
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
// Envoyer confirmation de lecture à l'expéditeur via WebSocket
|
|
||||||
// (sera aussi publié dans Kafka par MessageService)
|
|
||||||
UUID senderUUID = message.getSender().getId();
|
UUID senderUUID = message.getSender().getId();
|
||||||
String response = buildJsonMessage("read_receipt", Map.of(
|
long now = System.currentTimeMillis();
|
||||||
|
String timestampIso = java.time.Instant.ofEpochMilli(now).toString();
|
||||||
|
String response = buildJsonMessage("read", Map.of(
|
||||||
"messageId", messageUUID.toString(),
|
"messageId", messageUUID.toString(),
|
||||||
"readBy", userId,
|
"userId", userId,
|
||||||
"readAt", System.currentTimeMillis()
|
"timestamp", timestampIso
|
||||||
));
|
));
|
||||||
|
|
||||||
sendToUser(senderUUID, response);
|
sendToUser(senderUUID, response);
|
||||||
|
|||||||
@@ -3,48 +3,51 @@ package com.lions.dev.websocket.bridge;
|
|||||||
import com.lions.dev.dto.events.ChatMessageEvent;
|
import com.lions.dev.dto.events.ChatMessageEvent;
|
||||||
import com.lions.dev.websocket.ChatWebSocketNext;
|
import com.lions.dev.websocket.ChatWebSocketNext;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.time.Instant;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour le chat.
|
* Bridge qui consomme depuis Kafka et envoie via WebSocket pour le chat.
|
||||||
*
|
*
|
||||||
* Architecture:
|
* Architecture (best practice):
|
||||||
* MessageService → Kafka Topic (chat.messages) → Bridge → WebSocket → Client
|
* MessageService → Kafka Topic (chat.messages) → Bridge → WebSocket → Client
|
||||||
*/
|
*/
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ChatKafkaBridge {
|
public class ChatKafkaBridge {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: chat.messages");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consomme les messages chat depuis Kafka et les route vers WebSocket.
|
* Consomme les messages chat depuis Kafka et les route vers WebSocket.
|
||||||
*
|
|
||||||
* @param message Message Kafka contenant un ChatMessageEvent
|
|
||||||
* @return CompletionStage pour gérer l'ack/nack asynchrone
|
|
||||||
*/
|
*/
|
||||||
@Incoming("kafka-chat")
|
@Incoming("kafka-chat")
|
||||||
public CompletionStage<Void> processChatMessage(Message<ChatMessageEvent> message) {
|
public CompletionStage<Void> processChatMessage(Message<ChatMessageEvent> message) {
|
||||||
try {
|
try {
|
||||||
ChatMessageEvent event = message.getPayload();
|
ChatMessageEvent event = message.getPayload();
|
||||||
|
|
||||||
Log.debug("[CHAT-BRIDGE] Message reçu: " + event.getEventType() +
|
Log.debug("[CHAT-BRIDGE] Événement reçu: " + event.getEventType() +
|
||||||
" de " + event.getSenderId() + " à " + event.getRecipientId());
|
" de " + event.getSenderId() + " à " + event.getRecipientId());
|
||||||
|
|
||||||
UUID recipientId = UUID.fromString(event.getRecipientId());
|
UUID recipientId = UUID.fromString(event.getRecipientId());
|
||||||
|
|
||||||
// Construire le message JSON pour WebSocket
|
|
||||||
String wsMessage = buildWebSocketMessage(event);
|
String wsMessage = buildWebSocketMessage(event);
|
||||||
|
|
||||||
// Envoyer via WebSocket au destinataire
|
|
||||||
ChatWebSocketNext.sendMessageToUser(recipientId, wsMessage);
|
ChatWebSocketNext.sendMessageToUser(recipientId, wsMessage);
|
||||||
|
|
||||||
Log.debug("[CHAT-BRIDGE] Message routé vers WebSocket pour: " + event.getRecipientId());
|
Log.debug("[CHAT-BRIDGE] Message routé vers WebSocket pour: " + event.getRecipientId());
|
||||||
|
|
||||||
// Acknowledger le message Kafka
|
|
||||||
return message.ack();
|
return message.ack();
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -57,29 +60,66 @@ public class ChatKafkaBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construit le message JSON pour WebSocket à partir de l'événement Kafka.
|
* Construit le message JSON pour WebSocket (format attendu par le client Flutter).
|
||||||
*/
|
*/
|
||||||
private String buildWebSocketMessage(ChatMessageEvent event) {
|
private String buildWebSocketMessage(ChatMessageEvent event) {
|
||||||
try {
|
try {
|
||||||
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
com.fasterxml.jackson.databind.ObjectMapper mapper =
|
||||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
|
||||||
Map<String, Object> messageData = new java.util.HashMap<>();
|
String eventType = event.getEventType() != null ? event.getEventType() : "message";
|
||||||
messageData.put("id", event.getMessageId());
|
long ts = event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis();
|
||||||
messageData.put("conversationId", event.getConversationId());
|
String timestampIso = DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(ts));
|
||||||
messageData.put("senderId", event.getSenderId());
|
|
||||||
messageData.put("recipientId", event.getRecipientId());
|
Map<String, Object> data;
|
||||||
messageData.put("content", event.getContent());
|
String type;
|
||||||
messageData.put("timestamp", event.getTimestamp());
|
|
||||||
if (event.getMetadata() != null) {
|
switch (eventType) {
|
||||||
messageData.putAll(event.getMetadata());
|
case "delivery_confirmation":
|
||||||
|
type = "delivered";
|
||||||
|
data = new HashMap<>();
|
||||||
|
data.put("messageId", event.getMessageId());
|
||||||
|
data.put("isDelivered", event.getMetadata() != null &&
|
||||||
|
Boolean.TRUE.equals(event.getMetadata().get("isDelivered")));
|
||||||
|
data.put("timestamp", ts);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "read_confirmation":
|
||||||
|
type = "read";
|
||||||
|
data = new HashMap<>();
|
||||||
|
data.put("messageId", event.getMessageId());
|
||||||
|
data.put("userId", event.getMetadata() != null ?
|
||||||
|
event.getMetadata().get("readBy") : event.getSenderId());
|
||||||
|
data.put("timestamp", timestampIso);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "message":
|
||||||
|
default:
|
||||||
|
type = "message";
|
||||||
|
data = new HashMap<>();
|
||||||
|
data.put("id", event.getMessageId());
|
||||||
|
data.put("conversationId", event.getConversationId());
|
||||||
|
data.put("senderId", event.getSenderId());
|
||||||
|
data.put("recipientId", event.getRecipientId());
|
||||||
|
data.put("content", event.getContent() != null ? event.getContent() : "");
|
||||||
|
data.put("timestamp", timestampIso);
|
||||||
|
data.put("isRead", event.getMetadata() != null &&
|
||||||
|
Boolean.TRUE.equals(event.getMetadata().get("isRead")));
|
||||||
|
data.put("isDelivered", true);
|
||||||
|
if (event.getMetadata() != null) {
|
||||||
|
data.put("senderFirstName", event.getMetadata().getOrDefault("senderFirstName", ""));
|
||||||
|
data.put("senderLastName", event.getMetadata().getOrDefault("senderLastName", ""));
|
||||||
|
data.put("senderProfileImageUrl", event.getMetadata().getOrDefault("senderProfileImageUrl", ""));
|
||||||
|
data.put("attachmentUrl", event.getMetadata().getOrDefault("attachmentUrl", ""));
|
||||||
|
data.put("attachmentType", event.getMetadata().getOrDefault("attachmentType", "text"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
java.util.Map<String, Object> wsMessage = java.util.Map.of(
|
Map<String, Object> wsMessage = new HashMap<>();
|
||||||
"type", event.getEventType() != null ? event.getEventType() : "message",
|
wsMessage.put("type", type);
|
||||||
"data", messageData,
|
wsMessage.put("data", data);
|
||||||
"timestamp", event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis()
|
wsMessage.put("timestamp", ts);
|
||||||
);
|
|
||||||
|
|
||||||
return mapper.writeValueAsString(wsMessage);
|
return mapper.writeValueAsString(wsMessage);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.lions.dev.websocket.bridge;
|
|||||||
import com.lions.dev.dto.events.NotificationEvent;
|
import com.lions.dev.dto.events.NotificationEvent;
|
||||||
import com.lions.dev.websocket.NotificationWebSocketNext;
|
import com.lions.dev.websocket.NotificationWebSocketNext;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||||
@@ -25,6 +26,11 @@ import java.util.concurrent.CompletionStage;
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class NotificationKafkaBridge {
|
public class NotificationKafkaBridge {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: notifications");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consomme les événements depuis Kafka et les route vers WebSocket.
|
* Consomme les événements depuis Kafka et les route vers WebSocket.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.lions.dev.websocket.bridge;
|
|||||||
import com.lions.dev.dto.events.PresenceEvent;
|
import com.lions.dev.dto.events.PresenceEvent;
|
||||||
import com.lions.dev.websocket.NotificationWebSocketNext;
|
import com.lions.dev.websocket.NotificationWebSocketNext;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||||
@@ -23,6 +24,11 @@ import java.util.concurrent.CompletionStage;
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class PresenceKafkaBridge {
|
public class PresenceKafkaBridge {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: presence.updates");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consomme les événements de présence depuis Kafka et les route vers WebSocket.
|
* Consomme les événements de présence depuis Kafka et les route vers WebSocket.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.lions.dev.websocket.bridge;
|
|||||||
import com.lions.dev.dto.events.ReactionEvent;
|
import com.lions.dev.dto.events.ReactionEvent;
|
||||||
import com.lions.dev.websocket.NotificationWebSocketNext;
|
import com.lions.dev.websocket.NotificationWebSocketNext;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
import org.eclipse.microprofile.reactive.messaging.Incoming;
|
||||||
import org.eclipse.microprofile.reactive.messaging.Message;
|
import org.eclipse.microprofile.reactive.messaging.Message;
|
||||||
@@ -22,6 +23,11 @@ import java.util.concurrent.CompletionStage;
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ReactionKafkaBridge {
|
public class ReactionKafkaBridge {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
Log.info("[KAFKA-BRIDGE] Bridge démarré pour topic: reactions");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consomme les réactions depuis Kafka et les route vers WebSocket.
|
* Consomme les réactions depuis Kafka et les route vers WebSocket.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
# Ce fichier est automatiquement chargé avec: mvn quarkus:dev
|
# Ce fichier est automatiquement chargé avec: mvn quarkus:dev
|
||||||
# Les configurations ici surchargent celles de application.properties
|
# Les configurations ici surchargent celles de application.properties
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# Super administrateur (dev)
|
||||||
|
# ====================================================================
|
||||||
|
# En dev, clé par défaut pour PUT /users/{id}/role (header X-Super-Admin-Key).
|
||||||
|
# Saisir "dev-super-admin-key" dans l'app (Paramètres → Super Admin) pour attribuer des rôles.
|
||||||
|
afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:dev-super-admin-key}
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Base de données H2 (en mémoire)
|
# Base de données H2 (en mémoire)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
@@ -24,6 +31,12 @@ quarkus.hibernate-orm.packages=com.lions.dev.entity
|
|||||||
# Forcer la création du schéma au démarrage
|
# Forcer la création du schéma au démarrage
|
||||||
quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create
|
quarkus.hibernate-orm.schema-generation.scripts.action=drop-and-create
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# Kafka (développement local)
|
||||||
|
# ====================================================================
|
||||||
|
# En dev, défaut localhost:9092. Définir KAFKA_BOOTSTRAP_SERVERS si Kafka tourne ailleurs.
|
||||||
|
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Logging
|
# Logging
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# AfterWork Server - Configuration PRODUCTION
|
# AfterWork Server - Configuration PRODUCTION (profil prod)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Ce fichier est automatiquement chargé avec: java -jar app.jar
|
# Chargé avec QUARKUS_PROFILE=prod (Kubernetes ConfigMap).
|
||||||
# Les configurations ici surchargent celles de application.properties
|
# Ce fichier remplace application-production.properties pour cohérence
|
||||||
|
# avec le déploiement (QUARKUS_PROFILE=prod).
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# HTTP - Chemin de base de l'API
|
# HTTP - Chemin de base de l'API
|
||||||
@@ -19,34 +20,26 @@
|
|||||||
# - pathType: Prefix
|
# - pathType: Prefix
|
||||||
# - PAS d'annotation rewrite-target
|
# - PAS d'annotation rewrite-target
|
||||||
#
|
#
|
||||||
# Pourquoi cette approche ?
|
|
||||||
# - Swagger UI nécessite que l'application connaisse son contexte
|
|
||||||
# - Les URLs générées (OpenAPI, WebSocket) sont correctes
|
|
||||||
# - Cohérent avec les applications context-aware (btpxpress, etc.)
|
|
||||||
quarkus.http.root-path=/afterwork
|
quarkus.http.root-path=/afterwork
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Swagger/OpenAPI (Production)
|
# Swagger/OpenAPI (Production)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Configuration pour que Swagger UI fonctionne avec root-path
|
|
||||||
quarkus.swagger-ui.enable=true
|
quarkus.swagger-ui.enable=true
|
||||||
quarkus.swagger-ui.always-include=true
|
quarkus.swagger-ui.always-include=true
|
||||||
quarkus.swagger-ui.path=/q/swagger-ui
|
quarkus.swagger-ui.path=/q/swagger-ui
|
||||||
# Configuration du chemin OpenAPI (relatif au root-path)
|
|
||||||
quarkus.smallrye-openapi.path=/openapi
|
quarkus.smallrye-openapi.path=/openapi
|
||||||
# Configuration des serveurs OpenAPI pour que Swagger UI génère les bonnes URLs
|
quarkus.smallrye-openapi.servers=https://api.lions.dev
|
||||||
quarkus.smallrye-openapi.servers=https://api.lions.dev/afterwork
|
|
||||||
# Configuration explicite de l'URL OpenAPI pour Swagger UI
|
|
||||||
# Essayer avec URL absolue pour forcer le bon chemin
|
|
||||||
quarkus.swagger-ui.urls.default=https://api.lions.dev/afterwork/openapi
|
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Base de données PostgreSQL
|
# Base de données PostgreSQL
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
|
# IMPORTANT: Les credentials doivent être fournis via variables d'environnement
|
||||||
|
# en production (Kubernetes Secrets ou Vault).
|
||||||
quarkus.datasource.db-kind=postgresql
|
quarkus.datasource.db-kind=postgresql
|
||||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:postgresql-service.postgresql.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:mic-after-work-server-impl-quarkus-main}
|
||||||
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
quarkus.datasource.username=${DB_USERNAME:lionsuser}
|
||||||
quarkus.datasource.password=${DB_PASSWORD:LionsUser2025!}
|
quarkus.datasource.password=${DB_PASSWORD}
|
||||||
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
quarkus.datasource.jdbc.driver=org.postgresql.Driver
|
||||||
quarkus.datasource.jdbc.max-size=20
|
quarkus.datasource.jdbc.max-size=20
|
||||||
quarkus.datasource.jdbc.min-size=5
|
quarkus.datasource.jdbc.min-size=5
|
||||||
@@ -94,42 +87,18 @@ quarkus.log.category."io.quarkus".level=INFO
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Kafka Configuration (Production)
|
# Kafka Configuration (Production)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Kafka est deploye dans le namespace 'kafka' du cluster Kubernetes
|
|
||||||
# Service: kafka-service.kafka.svc.cluster.local:9092
|
|
||||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:kafka-service.kafka.svc.cluster.local:9092}
|
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:kafka-service.kafka.svc.cluster.local:9092}
|
||||||
|
|
||||||
# Configuration de resilience Kafka
|
|
||||||
mp.messaging.connector.smallrye-kafka.health-enabled=true
|
mp.messaging.connector.smallrye-kafka.health-enabled=true
|
||||||
mp.messaging.connector.smallrye-kafka.health-readiness-enabled=true
|
mp.messaging.connector.smallrye-kafka.health-readiness-enabled=true
|
||||||
|
|
||||||
# Topics auto-crees par Quarkus SmallRye Kafka:
|
|
||||||
# - notifications
|
|
||||||
# - chat.messages
|
|
||||||
# - reactions
|
|
||||||
# - presence.updates
|
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# WebSocket
|
# WebSocket / SSL / Performance / Localisation
|
||||||
# ====================================================================
|
|
||||||
# Note: La propriété quarkus.websocket.max-frame-size n'existe pas dans Quarkus 3.16
|
|
||||||
# Les WebSockets Next utilisent une configuration différente si nécessaire
|
|
||||||
|
|
||||||
# ====================================================================
|
|
||||||
# SSL/TLS (géré par le reverse proxy)
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
quarkus.http.ssl.certificate.files=
|
quarkus.http.ssl.certificate.files=
|
||||||
quarkus.http.ssl.certificate.key-files=
|
quarkus.http.ssl.certificate.key-files=
|
||||||
quarkus.http.insecure-requests=enabled
|
quarkus.http.insecure-requests=enabled
|
||||||
|
|
||||||
# ====================================================================
|
|
||||||
# Performance
|
|
||||||
# ====================================================================
|
|
||||||
quarkus.thread-pool.core-threads=2
|
quarkus.thread-pool.core-threads=2
|
||||||
quarkus.thread-pool.max-threads=16
|
quarkus.thread-pool.max-threads=16
|
||||||
quarkus.thread-pool.queue-size=100
|
quarkus.thread-pool.queue-size=100
|
||||||
|
|
||||||
# ====================================================================
|
|
||||||
# Localisation
|
|
||||||
# ====================================================================
|
|
||||||
quarkus.locales=fr-FR,en-US
|
quarkus.locales=fr-FR,en-US
|
||||||
quarkus.default-locale=fr-FR
|
quarkus.default-locale=fr-FR
|
||||||
@@ -17,6 +17,23 @@ quarkus.swagger-ui.always-include=true
|
|||||||
quarkus.swagger-ui.path=/q/swagger-ui
|
quarkus.swagger-ui.path=/q/swagger-ui
|
||||||
quarkus.smallrye-openapi.path=/openapi
|
quarkus.smallrye-openapi.path=/openapi
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# Super administrateur (créé au démarrage si absent)
|
||||||
|
# ====================================================================
|
||||||
|
# En production, définir via variables d'environnement (SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD).
|
||||||
|
afterwork.super-admin.email=${SUPER_ADMIN_EMAIL:superadmin@afterwork.lions.dev}
|
||||||
|
afterwork.super-admin.password=${SUPER_ADMIN_PASSWORD:SuperAdmin2025!}
|
||||||
|
afterwork.super-admin.first-name=${SUPER_ADMIN_FIRST_NAME:Super}
|
||||||
|
afterwork.super-admin.last-name=${SUPER_ADMIN_LAST_NAME:Administrator}
|
||||||
|
# Clé secrète pour les opérations admin (header X-Super-Admin-Key sur PUT /users/{id}/role, etc.)
|
||||||
|
afterwork.super-admin.api-key=${SUPER_ADMIN_API_KEY:}
|
||||||
|
|
||||||
|
# ====================================================================
|
||||||
|
# Wave API (paiement droits d'accès établissements)
|
||||||
|
# ====================================================================
|
||||||
|
wave.api.url=${WAVE_API_URL:https://api.wave.com}
|
||||||
|
wave.api.key=${WAVE_API_KEY:}
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# HTTP (commun à tous les environnements)
|
# HTTP (commun à tous les environnements)
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
@@ -62,24 +79,28 @@ kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:kafka-service.kafka.svc.cluste
|
|||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<EventType>
|
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<EventType>
|
||||||
# Topic: Notifications
|
# Topic: Notifications
|
||||||
|
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<NotificationEvent>
|
||||||
mp.messaging.outgoing.notifications.connector=smallrye-kafka
|
mp.messaging.outgoing.notifications.connector=smallrye-kafka
|
||||||
mp.messaging.outgoing.notifications.topic=notifications
|
mp.messaging.outgoing.notifications.topic=notifications
|
||||||
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<NotificationEvent>
|
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<NotificationEvent>
|
||||||
|
|
||||||
# Topic: Chat Messages
|
# Topic: Chat Messages
|
||||||
|
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<ChatMessageEvent>
|
||||||
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
|
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
|
||||||
mp.messaging.outgoing.chat-messages.topic=chat.messages
|
mp.messaging.outgoing.chat-messages.topic=chat.messages
|
||||||
mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
mp.messaging.outgoing.chat-messages.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ChatMessageEvent>
|
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ChatMessageEvent>
|
||||||
|
|
||||||
# Topic: Reactions (likes, comments, shares)
|
# Topic: Reactions (likes, comments, shares)
|
||||||
|
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<ReactionEvent>
|
||||||
mp.messaging.outgoing.reactions.connector=smallrye-kafka
|
mp.messaging.outgoing.reactions.connector=smallrye-kafka
|
||||||
mp.messaging.outgoing.reactions.topic=reactions
|
mp.messaging.outgoing.reactions.topic=reactions
|
||||||
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||||
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ReactionEvent>
|
# value.serializer omis - Quarkus génère automatiquement depuis Emitter<ReactionEvent>
|
||||||
|
|
||||||
# Topic: Presence Updates
|
# Topic: Presence Updates
|
||||||
|
# Note: Quarkus génère automatiquement les serializers Jackson basés sur le type Emitter<PresenceEvent>
|
||||||
mp.messaging.outgoing.presence.connector=smallrye-kafka
|
mp.messaging.outgoing.presence.connector=smallrye-kafka
|
||||||
mp.messaging.outgoing.presence.topic=presence.updates
|
mp.messaging.outgoing.presence.topic=presence.updates
|
||||||
mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
-- Migration V12: Nettoyage des colonnes legacy de la table users
|
||||||
|
-- Date: 2026-01-26
|
||||||
|
-- Description: Supprime les anciennes colonnes (nom, prenoms, mot_de_passe) qui coexistent
|
||||||
|
-- avec les nouvelles (first_name, last_name, password_hash) suite à la migration V3.
|
||||||
|
--
|
||||||
|
-- Contexte: La migration V3 devait renommer les colonnes, mais Hibernate avait déjà créé
|
||||||
|
-- les nouvelles colonnes. Résultat: la table avait les deux sets de colonnes.
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Étape 1: S'assurer que les nouvelles colonnes existent et ont des données
|
||||||
|
-- Si first_name est vide mais nom a des données, copier
|
||||||
|
UPDATE users SET first_name = nom
|
||||||
|
WHERE nom IS NOT NULL
|
||||||
|
AND nom != ''
|
||||||
|
AND (first_name IS NULL OR first_name = '');
|
||||||
|
|
||||||
|
UPDATE users SET last_name = prenoms
|
||||||
|
WHERE prenoms IS NOT NULL
|
||||||
|
AND prenoms != ''
|
||||||
|
AND (last_name IS NULL OR last_name = '');
|
||||||
|
|
||||||
|
UPDATE users SET password_hash = mot_de_passe
|
||||||
|
WHERE mot_de_passe IS NOT NULL
|
||||||
|
AND mot_de_passe != ''
|
||||||
|
AND (password_hash IS NULL OR password_hash = '');
|
||||||
|
|
||||||
|
-- Étape 2: Rendre les anciennes colonnes nullable (si elles existent et sont NOT NULL)
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'nom') THEN
|
||||||
|
ALTER TABLE users ALTER COLUMN nom DROP NOT NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'prenoms') THEN
|
||||||
|
ALTER TABLE users ALTER COLUMN prenoms DROP NOT NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'mot_de_passe') THEN
|
||||||
|
ALTER TABLE users ALTER COLUMN mot_de_passe DROP NOT NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Étape 3: Supprimer les anciennes colonnes (optionnel - décommenter si vous voulez un schéma propre)
|
||||||
|
-- Note: Gardées pour l'instant pour compatibilité avec d'éventuels anciens clients
|
||||||
|
/*
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'nom') THEN
|
||||||
|
ALTER TABLE users DROP COLUMN nom;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'prenoms') THEN
|
||||||
|
ALTER TABLE users DROP COLUMN prenoms;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'users' AND column_name = 'mot_de_passe') THEN
|
||||||
|
ALTER TABLE users DROP COLUMN mot_de_passe;
|
||||||
|
END IF;
|
||||||
|
*/
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Commentaire explicatif
|
||||||
|
COMMENT ON TABLE users IS 'Table utilisateurs v2.0 - Colonnes legacy (nom, prenoms, mot_de_passe) dépréciées, utiliser first_name, last_name, password_hash';
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- Migration V13: Abonnements et paiements établissements (Wave)
|
||||||
|
-- Date: 2026-01-28
|
||||||
|
-- Description: Tables pour les droits d'accès des établissements payés via Wave
|
||||||
|
|
||||||
|
-- Abonnements établissements
|
||||||
|
CREATE TABLE IF NOT EXISTS establishment_subscriptions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
establishment_id UUID NOT NULL,
|
||||||
|
plan VARCHAR(20) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
wave_session_id VARCHAR(255),
|
||||||
|
amount_xof INTEGER,
|
||||||
|
paid_at TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT fk_establishment_subscriptions_establishment
|
||||||
|
FOREIGN KEY (establishment_id) REFERENCES establishments(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT chk_establishment_subscriptions_plan
|
||||||
|
CHECK (plan IN ('MONTHLY', 'YEARLY')),
|
||||||
|
CONSTRAINT chk_establishment_subscriptions_status
|
||||||
|
CHECK (status IN ('PENDING', 'ACTIVE', 'EXPIRED', 'CANCELLED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishment_subscriptions_establishment
|
||||||
|
ON establishment_subscriptions(establishment_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishment_subscriptions_status
|
||||||
|
ON establishment_subscriptions(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishment_subscriptions_wave_session
|
||||||
|
ON establishment_subscriptions(wave_session_id);
|
||||||
|
|
||||||
|
-- Paiements établissements (historique Wave)
|
||||||
|
CREATE TABLE IF NOT EXISTS establishment_payments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
establishment_id UUID NOT NULL,
|
||||||
|
amount_xof INTEGER NOT NULL,
|
||||||
|
wave_session_id VARCHAR(255),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
client_phone VARCHAR(30),
|
||||||
|
plan VARCHAR(20),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT fk_establishment_payments_establishment
|
||||||
|
FOREIGN KEY (establishment_id) REFERENCES establishments(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT chk_establishment_payments_status
|
||||||
|
CHECK (status IN ('PENDING', 'COMPLETED', 'FAILED', 'CANCELLED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishment_payments_establishment
|
||||||
|
ON establishment_payments(establishment_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishment_payments_wave_session
|
||||||
|
ON establishment_payments(wave_session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishment_payments_status
|
||||||
|
ON establishment_payments(status);
|
||||||
|
|
||||||
|
COMMENT ON TABLE establishment_subscriptions IS 'Abonnements / droits d''accès des établissements (paiement Wave)';
|
||||||
|
COMMENT ON TABLE establishment_payments IS 'Historique des paiements Wave pour les établissements';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- V14: Ajout is_active pour Manager & Abonnement (suspension automatique Wave)
|
||||||
|
-- users.is_active : false = manager suspendu (paiement échoué/annulé)
|
||||||
|
-- establishments.is_active : false = établissement masqué (abonnement inactif)
|
||||||
|
|
||||||
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
ALTER TABLE establishments ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN users.is_active IS 'false = compte suspendu (ex: manager abonnement expiré)';
|
||||||
|
COMMENT ON COLUMN establishments.is_active IS 'false = établissement masqué (abonnement inactif)';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
-- Migration V15: Création de la table social_posts (Publications)
|
||||||
|
-- Date: 2026-01-28
|
||||||
|
-- Description: Table des posts sociaux pour le fil d'actualité AfterWork (correction 500)
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS social_posts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
content VARCHAR(2000) NOT NULL,
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
image_url VARCHAR(500),
|
||||||
|
likes_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
comments_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shares_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
CONSTRAINT fk_social_posts_user
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_social_posts_user_id ON social_posts(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_social_posts_created_at ON social_posts(created_at DESC);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION update_social_posts_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_update_social_posts_updated_at
|
||||||
|
BEFORE UPDATE ON social_posts
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_social_posts_updated_at();
|
||||||
|
|
||||||
|
COMMENT ON TABLE social_posts IS 'Publications (posts) sociaux du fil AfterWork';
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
-- V1_1__Create_Base_Tables_For_Fresh_Db.sql
|
||||||
|
-- Création des tables de base pour une base de données vierge.
|
||||||
|
-- À exécuter après V1__Baseline lorsque le schéma n'a pas été créé par Hibernate.
|
||||||
|
-- Les migrations V2, V3, V4, V7+ supposent que users, establishments et events existent.
|
||||||
|
|
||||||
|
-- Table users (structure minimale attendue par V3 et l'entité Users)
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
first_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
last_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
role VARCHAR(50) NOT NULL,
|
||||||
|
profile_image_url VARCHAR(500),
|
||||||
|
bio VARCHAR(500),
|
||||||
|
loyalty_points INTEGER NOT NULL DEFAULT 0,
|
||||||
|
preferences JSONB NOT NULL DEFAULT '{}',
|
||||||
|
is_verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
is_online BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
last_seen TIMESTAMP,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table establishments (V4 renomme total_ratings_count -> total_reviews_count)
|
||||||
|
CREATE TABLE IF NOT EXISTS establishments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(100) NOT NULL,
|
||||||
|
address VARCHAR(500) NOT NULL,
|
||||||
|
city VARCHAR(100) NOT NULL,
|
||||||
|
postal_code VARCHAR(20) NOT NULL,
|
||||||
|
description VARCHAR(2000),
|
||||||
|
phone_number VARCHAR(50),
|
||||||
|
website VARCHAR(500),
|
||||||
|
average_rating DOUBLE PRECISION,
|
||||||
|
total_ratings_count INTEGER,
|
||||||
|
price_range VARCHAR(20),
|
||||||
|
verification_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
latitude DOUBLE PRECISION,
|
||||||
|
longitude DOUBLE PRECISION,
|
||||||
|
manager_id UUID NOT NULL REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table events (structure minimale ; V2 et V7 ajoutent les colonnes supplémentaires)
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description VARCHAR(1000),
|
||||||
|
start_date TIMESTAMP NOT NULL,
|
||||||
|
end_date TIMESTAMP NOT NULL,
|
||||||
|
category VARCHAR(100),
|
||||||
|
link VARCHAR(500),
|
||||||
|
image_url VARCHAR(500),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'OPEN',
|
||||||
|
creator_id UUID NOT NULL REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table de jointure event_participants (events <-> users)
|
||||||
|
CREATE TABLE IF NOT EXISTS event_participants (
|
||||||
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (event_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table de jointure user_favorite_events (users <-> events)
|
||||||
|
CREATE TABLE IF NOT EXISTS user_favorite_events (
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (user_id, event_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index pour les FK et recherches courantes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_establishments_manager ON establishments(manager_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_creator ON events(creator_id);
|
||||||
@@ -1,13 +1,38 @@
|
|||||||
-- Migration V3: Migration de la table users vers l'architecture v2.0
|
-- Migration V3: Migration de la table users vers l'architecture v2.0
|
||||||
-- Date: 2026-01-15
|
-- Date: 2026-01-15
|
||||||
-- Description: Renommage des colonnes et ajout des nouveaux champs pour l'entité User
|
-- Description: Renommage des colonnes et ajout des nouveaux champs pour l'entité User
|
||||||
|
-- Note: Migration rendue idempotente pour éviter les conflits avec Hibernate
|
||||||
|
|
||||||
-- Renommer les colonnes existantes
|
-- Renommer/migrer les colonnes existantes (idempotent)
|
||||||
ALTER TABLE users RENAME COLUMN nom TO first_name;
|
DO $$
|
||||||
ALTER TABLE users RENAME COLUMN prenoms TO last_name;
|
BEGIN
|
||||||
ALTER TABLE users RENAME COLUMN mot_de_passe TO password_hash;
|
-- Si first_name n'existe pas mais nom existe, renommer
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='first_name')
|
||||||
|
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='nom') THEN
|
||||||
|
ALTER TABLE users RENAME COLUMN nom TO first_name;
|
||||||
|
-- Si first_name n'existe pas du tout, le créer
|
||||||
|
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='first_name') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN first_name VARCHAR(100) NOT NULL DEFAULT '';
|
||||||
|
END IF;
|
||||||
|
|
||||||
-- Ajouter les nouvelles colonnes
|
-- Si last_name n'existe pas mais prenoms existe, renommer
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='last_name')
|
||||||
|
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='prenoms') THEN
|
||||||
|
ALTER TABLE users RENAME COLUMN prenoms TO last_name;
|
||||||
|
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='last_name') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN last_name VARCHAR(100) NOT NULL DEFAULT '';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Si password_hash n'existe pas mais mot_de_passe existe, renommer
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='password_hash')
|
||||||
|
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='mot_de_passe') THEN
|
||||||
|
ALTER TABLE users RENAME COLUMN mot_de_passe TO password_hash;
|
||||||
|
ELSIF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='password_hash') THEN
|
||||||
|
ALTER TABLE users ADD COLUMN password_hash VARCHAR(255) NOT NULL DEFAULT '';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Ajouter les nouvelles colonnes (déjà idempotent)
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS bio VARCHAR(500);
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS bio VARCHAR(500);
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS loyalty_points INTEGER DEFAULT 0 NOT NULL;
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS loyalty_points INTEGER DEFAULT 0 NOT NULL;
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- EXECUTE PROCEDURE pour compatibilité PostgreSQL < 11 (EXECUTE FUNCTION à partir de PG 11)
|
||||||
CREATE TRIGGER trigger_update_business_hours_updated_at
|
CREATE TRIGGER trigger_update_business_hours_updated_at
|
||||||
BEFORE UPDATE ON business_hours
|
BEFORE UPDATE ON business_hours
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION update_business_hours_updated_at();
|
EXECUTE PROCEDURE update_business_hours_updated_at();
|
||||||
|
|
||||||
-- Commentaires pour documentation
|
-- Commentaires pour documentation
|
||||||
COMMENT ON TABLE business_hours IS 'Horaires d''ouverture des établissements (v2.0)';
|
COMMENT ON TABLE business_hours IS 'Horaires d''ouverture des établissements (v2.0)';
|
||||||
|
|||||||
Reference in New Issue
Block a user