Compare commits

2 Commits

Author SHA1 Message Date
gbanedahoud
398b2b13d2 Merge pull request #2 from gbanedahoud/develop
Develop
2024-11-17 22:59:47 +00:00
gbanedahoud
b7bbc2d61d Merge pull request #1 from gbanedahoud/develop
Develop Clean
2024-09-11 19:29:36 +00:00
254 changed files with 977 additions and 35981 deletions

View File

@@ -1,48 +1,5 @@
# Build artifacts
# Note: target/ et *.jar sont nécessaires pour le Docker build
# car on copie le JAR runner depuis target/
*.war
*.ear
# IDE
.idea/
*.iml
.vscode/
.settings/
.project
.classpath
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
.github/
# Documentation
*.md
docs/
README.md
# Tests
**/test/
**/*Test.java
**/*TestCase.java
# Logs
*.log
logs/
# Temporaires
*.tmp
*.temp
tmp/
temp/
# Scripts et Docker (hors contexte utile pour le build)
scripts/
docker/
*
!target/*-runner
!target/*-runner.jar
!target/lib/*
!target/quarkus-app/*

View File

@@ -1,129 +0,0 @@
# ============================================
# AfterWork API - Configuration
# ============================================
# Copiez ce fichier vers .env et remplissez les valeurs appropriées
# NE JAMAIS COMMITTER le fichier .env !
#
# ==== INFRASTRUCTURE LIONS (Production) ====
# - API Gateway: https://api.lions.dev/afterwork
# - PostgreSQL: postgresql-service.postgresql.svc.cluster.local:5432
# - Kafka: kafka-service.kafka.svc.cluster.local:9092
# - Prometheus: https://prometheus.lions.dev
# - Grafana: https://grafana.lions.dev
# - Vault: https://vault.lions.dev
# - Keycloak: https://security.lions.dev
# ============================================
# BASE DE DONNÉES
# ============================================
# === Développement local ===
DB_HOST=localhost
DB_PORT=5432
DB_NAME=afterwork_dev
DB_USERNAME=skyfile
DB_PASSWORD=skyfile
# === Production Lions (via Kubernetes Secrets) ===
# DB_HOST=postgresql-service.postgresql.svc.cluster.local
# DB_PORT=5432
# DB_NAME=mic-after-work-server-impl-quarkus-main
# DB_USERNAME=lionsuser
# DB_PASSWORD=<voir-kubernetes-secrets>
# ============================================
# JWT / SÉCURITÉ
# ============================================
# Secret pour signer les tokens JWT (minimum 32 caractères)
# Générez avec: openssl rand -base64 32
JWT_SECRET=afterwork-jwt-secret-min-32-bytes-for-hs256!
JWT_LIFESPAN=86400
# IMPORTANT: L'issuer doit être "afterwork" (correspondant à JwtService.ISSUER)
JWT_ISSUER=afterwork
# ============================================
# SUPER ADMIN
# ============================================
SUPER_ADMIN_EMAIL=superadmin@afterwork.lions.dev
SUPER_ADMIN_PASSWORD=SuperAdmin2025!
SUPER_ADMIN_API_KEY=dev-super-admin-key
# ============================================
# EMAIL (SMTP)
# ============================================
# Mode mock pour le développement (pas d'envoi réel)
MAILER_MOCK=true
MAILER_HOST=smtp.gmail.com
MAILER_PORT=587
MAILER_USERNAME=noreply@afterwork.ci
MAILER_PASSWORD=CHANGEZ_MOI_SMTP_PASSWORD
MAILER_FROM=AfterWork <noreply@afterwork.ci>
# ============================================
# KAFKA
# ============================================
# === Développement local ===
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
# === Production Lions ===
# KAFKA_BOOTSTRAP_SERVERS=kafka-service.kafka.svc.cluster.local:9092
# === Confluent Cloud (optionnel) ===
# KAFKA_BOOTSTRAP_SERVERS=pkc-xxxxx.region.provider.confluent.cloud:9092
# KAFKA_SECURITY_PROTOCOL=SASL_SSL
# KAFKA_SASL_MECHANISM=PLAIN
# KAFKA_SASL_USERNAME=YOUR_API_KEY
# KAFKA_SASL_PASSWORD=YOUR_API_SECRET
# ============================================
# WAVE PAYMENT
# ============================================
WAVE_BASE_URL=https://api.wave.com
WAVE_API_KEY=VOTRE_CLE_API_WAVE
WAVE_SECRET=VOTRE_SECRET_WAVE
WAVE_CURRENCY=XOF
WAVE_CALLBACK_URL=https://api.lions.dev/afterwork/webhooks/wave
# ============================================
# RATE LIMITING
# ============================================
AFTERWORK_RATELIMIT_MAX_REQUESTS=10
AFTERWORK_RATELIMIT_WINDOW_SECONDS=60
# ============================================
# QUARKUS
# ============================================
QUARKUS_PROFILE=dev
QUARKUS_PACKAGE_TYPE=fast-jar
QUARKUS_LOG_LEVEL=INFO
QUARKUS_LOG_CONSOLE_JSON=false
# CORS (développement)
QUARKUS_HTTP_CORS=true
QUARKUS_HTTP_CORS_ORIGINS=http://localhost:3000,http://localhost:4200
# ============================================
# OBSERVABILITÉ
# ============================================
# Métriques Prometheus (auto-découverte via annotations K8s)
QUARKUS_MICROMETER_EXPORT_PROMETHEUS_ENABLED=true
# Health checks
QUARKUS_SMALLRYE_HEALTH_UI_ENABLE=true
# ============================================
# DÉPLOIEMENT LIONS (lionsctl)
# ============================================
# Pour déployer avec lionsctl:
# lionsctl pipeline \
# -u https://git.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main \
# -b develop \
# -j 17 \
# -e production \
# -c k2 \
# -m dadyo@lions.dev
# Variables d'environnement requises pour lionsctl:
# LIONS_REGISTRY_USERNAME=lionsregistry
# LIONS_REGISTRY_PASSWORD=<votre-mot-de-passe>
# LIONS_GITEA_USERNAME=lionsctl-bot
# LIONS_GITEA_PASSWORD=lionsctl-bot@2025

86
.gitignore vendored
View File

@@ -1,105 +1,43 @@
# ====================
# Maven
# ====================
#Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
release.properties
.flattened-pom.xml
dependency-reduced-pom.xml
# ====================
# IDE - Eclipse
# ====================
# Eclipse
.project
.classpath
.settings/
bin/
# ====================
# IDE - IntelliJ IDEA
# ====================
.idea/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
out/
# ====================
# IDE - NetBeans
# ====================
# NetBeans
nb-configuration.xml
# ====================
# IDE - Visual Studio Code
# ====================
.vscode/
# Visual Studio Code
.vscode
.factorypath
# ====================
# OS - macOS
# ====================
# OSX
.DS_Store
._*
# ====================
# OS - Windows
# ====================
Thumbs.db
Desktop.ini
ehthumbs.db
# ====================
# Vim / Editors
# ====================
# Vim
*.swp
*.swo
*~
# ====================
# Patch files
# ====================
# patch
*.orig
*.rej
# ====================
# Environment & Secrets
# ====================
# Local environment
.env
.env.*
!.env.example
application-local.properties
*-secrets.yaml
*.pem
*.key
*.p12
*.jks
# JWT secret key (ne pas committer en prod!)
src/main/resources/META-INF/jwt-secret.key
# ====================
# Quarkus
# ====================
# Plugin directory
/.quarkus/cli/plugins/
.certs/
# ====================
# Logs
# ====================
*.log
logs/
hs_err_pid*.log
replay_pid*.log
backend_log.txt
# ====================
# Test output
# ====================
test-output/
surefire-reports/
# ====================
# Docker (local)
# ====================
docker-compose.override.yml

View File

@@ -1,4 +0,0 @@
-Xmx2048m
-Xms1024m
-XX:MaxMetaspaceSize=512m
-Dfile.encoding=UTF-8

View File

@@ -21,72 +21,77 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.ThreadLocalRandom;
public final class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "3.3.2";
public final class MavenWrapperDownloader
{
private static final String WRAPPER_VERSION = "3.2.0";
private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE"));
private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) );
public static void main(String[] args) {
log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION);
public static void main( String[] args )
{
log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION );
if (args.length != 2) {
System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing");
System.exit(1);
if ( args.length != 2 )
{
System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" );
System.exit( 1 );
}
try {
log(" - Downloader started");
final URL wrapperUrl = URI.create(args[0]).toURL();
final String jarPath = args[1].replace("..", ""); // Sanitize path
final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize();
downloadFileFromURL(wrapperUrl, wrapperJarPath);
log("Done");
} catch (IOException e) {
System.err.println("- Error downloading: " + e.getMessage());
if (VERBOSE) {
try
{
log( " - Downloader started" );
final URL wrapperUrl = new URL( args[0] );
final String jarPath = args[1].replace( "..", "" ); // Sanitize path
final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize();
downloadFileFromURL( wrapperUrl, wrapperJarPath );
log( "Done" );
}
catch ( IOException e )
{
System.err.println( "- Error downloading: " + e.getMessage() );
if ( VERBOSE )
{
e.printStackTrace();
}
System.exit(1);
System.exit( 1 );
}
}
private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath)
throws IOException {
log(" - Downloading to: " + wrapperJarPath);
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
final String username = System.getenv("MVNW_USERNAME");
final char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
Authenticator.setDefault(new Authenticator() {
private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath )
throws IOException
{
log( " - Downloading to: " + wrapperJarPath );
if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null )
{
final String username = System.getenv( "MVNW_USERNAME" );
final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray();
Authenticator.setDefault( new Authenticator()
{
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
protected PasswordAuthentication getPasswordAuthentication()
{
return new PasswordAuthentication( username, password );
}
});
} );
}
Path temp = wrapperJarPath
.getParent()
.resolve(wrapperJarPath.getFileName() + "."
+ Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
try (InputStream inStream = wrapperUrl.openStream()) {
Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING);
Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(temp);
try ( InputStream inStream = wrapperUrl.openStream() )
{
Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING );
}
log(" - Downloader complete");
log( " - Downloader complete" );
}
private static void log(String msg) {
if (VERBOSE) {
System.out.println(msg);
private static void log( String msg )
{
if ( VERBOSE )
{
System.out.println( msg );
}
}

View File

@@ -14,7 +14,5 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=source
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar

View File

@@ -1,198 +0,0 @@
# Audit intégral Frontend (Flutter) & Backend (Quarkus)
**Date** : 4 février 2026
**Périmètre** : `afterwork` (Flutter), `mic-after-work-server-impl-quarkus-main` (Quarkus)
**Références** : bonnes pratiques Quarkus REST, Flutter clean architecture, REST/JWT, WebSocket, Kafka (recherches web et documentation officielle).
---
## 1. Résumé exécutif
| Domaine | État global | Points critiques |
|--------|-------------|------------------|
| **Sécurité API (auth/authz)** | Critique | Aucune vérification JWT ; userId pris de lURL/body sans preuve didentité |
| **Couches backend** | Partiel | Resource accède parfois au repository ; validation incohérente (manuel vs Bean Validation) |
| **Gestion derreurs backend** | Partiel | Réponse derreur JSON construite à la main (risque dinjection) ; exceptions métier non gérées |
| **Frontend auth** | Critique | Aucun en-tête `Authorization` sur les requêtes API |
| **Frontend architecture** | Correct | data/domain/presentation présents ; pas de couche use-case systématique |
| **WebSocket** | Correct | Heartbeat + reconnexion présents ; pas de backoff exponentiel côté Flutter |
---
## 2. Backend (Quarkus) Analyse détaillée
### 2.1 Architecture des couches
**Recommandation (bonnes pratiques Quarkus)** :
Resource (REST, DTO) → Service (métier) → Repository (persistance). La resource ne doit pas appeler le repository pour de la logique métier.
**Constat :**
- **MessageResource** (lignes 98104, 168174) : appelle directement `usersRepository.findById(userId)` pour vérifier lexistence de lutilisateur, au lieu de déléguer au service (ex. `messageService.getUserConversations(userId)` qui lèverait `UserNotFoundException`).
- **MessageResource** injecte `UsersRepository` en plus de `MessageService` → mélange des responsabilités et duplication de la règle “utilisateur doit exister”.
**Recommandation :** Déplacer la résolution/utilisateur dans `MessageService` et faire lever `UserNotFoundException` ; supprimer linjection de `UsersRepository` dans `MessageResource`.
---
### 2.2 Validation des entrées
**Recommandation :** Utiliser Bean Validation (Hibernate Validator) sur les DTO avec `@Valid` sur les paramètres des endpoints. Éviter la validation manuelle dans la resource.
**Constat :**
- **SendMessageRequestDTO** : pas dannotations `@NotNull`, `@NotBlank` ; validation manuelle via `isValid()`.
- **MessageResource.sendMessage** : pas de `@Valid` ; utilise `request.isValid()` et retourne 400 manuellement.
- Autres ressources (UsersResource, EstablishmentResource, FriendshipResource, etc.) : utilisent correctement `@Valid` et DTO avec contraintes.
**Recommandation :** Ajouter sur `SendMessageRequestDTO` les annotations (`@NotNull` pour senderId, recipientId, `@NotBlank` pour content, etc.) et appeler lendpoint avec `@Valid SendMessageRequestDTO request`. Supprimer `isValid()` et le bloc manuel 400.
---
### 2.3 Authentification et autorisation
**Recommandation (OWASP / JWT)** :
Chaque endpoint protégé doit valider un JWT (ou session) et dériver lidentité du token. Les paramètres comme `userId` dans lURL ne doivent pas être la seule source de vérité : vérifier que le sujet du token correspond à la ressource demandée.
**Constat :**
- Aucun usage de `@RolesAllowed`, `@PermitAll`, ni de filtre/filtre JWT dans le projet.
- Les endpoints utilisent `userId` en `@PathParam` (ex. `/notifications/user/{userId}`, `/messages/conversations/{userId}`) ou dans le body (ex. `SendMessageRequestDTO.senderId`) sans aucune preuve que lappelant est cet utilisateur.
- Le commentaire dans `NotificationResource` indique : *“En production, le userId doit être dérivé du contexte d'authentification (JWT/session), pas de l'URL.”* → non implémenté.
**Impact :** Un attaquant peut lire/modifier les données dun autre utilisateur en devinant ou en énumérant des UUID.
**Recommandation :** Introduire lauthentification JWT (ex. `quarkus-oidc` ou filtre custom), extraire le `userId` (ou subject) du token, et pour chaque endpoint : soit utiliser ce `userId` comme source de vérité, soit vérifier que le `userId` en path/body est égal au sujet du token (pour les rôles appropriés).
---
### 2.4 Gestion globale des exceptions
**Recommandation :** Un seul point de sortie pour les erreurs (ExceptionMapper), réponses en JSON structuré (ex. `{"error": "..."}`) avec échappement correct. Gérer toutes les exceptions métier connues.
**Constat :**
- **GlobalExceptionHandler** : gère `BadRequestException`, `UserNotFoundException`, `EventNotFoundException`, `NotFoundException`, `UnauthorizedException`, `ServerException`, `RuntimeException`, et cas par défaut.
- **FriendshipNotFoundException** et **EstablishmentHasDependenciesException** ne sont pas gérées explicitement → elles tombent dans `RuntimeException` ou “Unexpected error”, avec un message potentiellement générique ou une stack trace.
- **buildResponse** (ligne 6265) :
`entity("{\"error\":\"" + message + "\"}")`
Concaténation directe de `message` dans le JSON. Si `message` contient `"` ou `\`, le JSON est mal formé et peut poser des risques (injection / parsing côté client). Il faut sérialiser le message en JSON (ex. via Jackson/JSON-B) au lieu de concaténer une chaîne.
**Recommandation :**
1) Ajouter des branches pour `FriendshipNotFoundException` (ex. 404) et `EstablishmentHasDependenciesException` (ex. 409 Conflict).
2) Remplacer la concaténation par un DTO derreur sérialisé (ex. `Map.of("error", message)` ou classe dédiée) avec le moteur JSON du framework.
---
### 2.5 Ressources qui gèrent les erreurs en local
**Recommandation :** La resource ne doit pas faire de try/catch générique qui transforme tout en 500. Elle doit déléguer au service ; les exceptions métier doivent être mappées par le GlobalExceptionHandler.
**Constat :**
- **MessageResource** : plusieurs méthodes avec `try { ... } catch (Exception e) { return 500 ... }`. Les exceptions métier (ex. utilisateur inexistant, conversation inexistante) ne sont pas levées sous forme dexceptions typées ; elles sont noyées dans un message générique 500.
**Recommandation :** Faire lever par le service des exceptions métier (ex. `UserNotFoundException`, `NotFoundException`) et supprimer les try/catch larges dans la resource ; laisser le GlobalExceptionHandler produire 404/400/500 de façon cohérente.
---
### 2.6 Kafka (déjà traité)
- Tuning prod (`max.poll.interval.ms`, `max.poll.records`, `session.timeout.ms`) déjà ajouté dans `application-prod.properties`.
- Bonnes pratiques SmallRye : en cas déchec critique après consommation, envisager `message.nack()` et stratégie de commit manuel si nécessaire (au-delà du scope de cet audit).
---
## 3. Frontend (Flutter) Analyse détaillée
### 3.1 Structure (clean architecture)
**Recommandation :** Séparation nette data / domain / presentation ; repositories en abstraction dans domain ; use cases optionnels mais utiles pour une logique métier réutilisable.
**Constat :**
- Présence de `data/` (datasources, models, repositories impl, services), `domain/` (entities, repositories abstraits, usecases partiels), `presentation/` (screens, state_management avec BLoC).
- Les datasources sont bien séparés ; les repositories implémentent les contrats du domain. Use cases présents seulement pour une partie des flux (ex. `get_user`).
**Verdict :** Conforme à une clean architecture légère. On peut étendre progressivement les use cases pour les flux critiques.
---
### 3.2 Appels API et authentification
**Recommandation :** Toute requête vers une API protégée doit envoyer le token (ex. `Authorization: Bearer <token>`). Le token doit être lu depuis un stockage sécurisé et rafraîchi si nécessaire.
**Constat :**
- Aucun datasource (user, notification, chat, event, social, reservation, establishment, etc.) najoute den-tête `Authorization` ou `Bearer`.
- Les headers utilisés sont principalement `Content-Type` et `Accept`. Aucune utilisation de `SecureStorage` (ou équivalent) pour récupérer un token et lattacher aux requêtes.
- LAPI backend nexige aujourdhui pas de JWT ; en revanche, dès que lauth sera activée côté backend, tous les appels devront envoyer le token.
**Recommandation :**
1) Créer un client HTTP unique (wrapper ou interceptor) qui récupère le token (ex. depuis `SecureStorage`) et ajoute `Authorization: Bearer <token>` à chaque requête.
2) Utiliser ce client dans tous les datasources au lieu dutiliser `http.Client` brut sans headers dauth.
3) Gérer le cas “token absent ou expiré” (401) : redirection vers login ou refresh.
---
### 3.3 WebSocket (notifications et chat)
**Recommandation (bonnes pratiques WebSocket)** : Heartbeat régulier, reconnexion avec backoff exponentiel, file dattente des messages en cas de déconnexion si besoin.
**Constat :**
- **RealtimeNotificationService** et **ChatWebSocketService** :
- Connexion avec `WebSocketChannel.connect`.
- Heartbeat toutes les 30 s (`_heartbeatInterval`).
- Reconnexion avec délai fixe (`_initialReconnectDelay = 5 s`) et plafond de tentatives (`_maxReconnectAttempts = 5`).
- Pas de backoff exponentiel (délai constant entre les tentatives). Pour réduire la charge serveur en cas de panne, un backoff exponentiel est préférable.
**Recommandation :** Conserver le heartbeat et la reconnexion ; ajouter un backoff exponentiel (ex. 2s, 4s, 8s, 16s, 30s) pour les tentatives de reconnexion, avec un plafond (ex. 30 s).
---
### 3.4 Gestion des erreurs et parsing
- Les datasources gèrent les timeouts, `SocketException`, et codes HTTP (401, 404, etc.) et lèvent des exceptions métier (ex. `ServerException`, `UnauthorizedException`). Cest cohérent.
- Vérifier que partout où lon parse le body derreur, on utilise une clé unique (ex. `error` ou `message`) alignée avec le backend. Après correction du backend (réponse derreur en JSON structuré), adapter si nécessaire le parsing côté Flutter pour lire `error` ou `message`.
---
## 4. Tableau de synthèse des écarts
| # | Composant | Écart | Sévérité | Action recommandée |
|---|-----------|--------|----------|---------------------|
| 1 | Backend | Aucune auth JWT ; userId pris de lURL/body sans preuve | Critique | Introduire JWT et dériver userId du token |
| 2 | Frontend | Aucun en-tête Authorization sur les requêtes API | Critique | Client HTTP centralisé avec Bearer token |
| 3 | Backend | MessageResource : accès direct au repository + validation manuelle | Moyen | Déléguer au service ; Bean Validation sur SendMessageRequestDTO |
| 4 | Backend | buildResponse : concaténation JSON pour le message derreur | Moyen | Utiliser un DTO/Map sérialisé en JSON |
| 5 | Backend | FriendshipNotFoundException, EstablishmentHasDependenciesException non gérées dans GlobalExceptionHandler | Moyen | Ajouter les branches et codes HTTP appropriés |
| 6 | Backend | MessageResource : try/catch générique qui masque les exceptions métier | Moyen | Lever des exceptions typées et laisser le handler global gérer |
| 7 | Frontend | Reconnexion WebSocket avec délai fixe | Faible | Implémenter backoff exponentiel |
---
## 5. Bonnes pratiques croisées (références)
- **Quarkus REST** : Resource → Service → Repository ; DTO + `@Valid` ; ExceptionMapper unique ; pas de logique métier dans la resource.
- **Sécurité REST/JWT** : Vérifier le token sur chaque requête ; ne pas faire confiance au userId passé par le client pour lautorisation.
- **Flutter** : Clean architecture avec repositories abstraits ; couche data qui envoie toujours lauth (client commun avec token).
- **WebSocket** : Heartbeat + reconnexion avec backoff exponentiel pour limiter la charge et les reconnexions agressives.
---
## 6. Conclusion
Les points les plus critiques concernent **lauthentification et lautorisation** : côté backend, aucun contrôle sur lidentité de lappelant ; côté frontend, aucun token nest envoyé. La cohérence des couches (resource sans accès direct au repository pour la logique métier), la validation (Bean Validation partout, y compris chat), et la gestion derreurs (réponse JSON sûre, exceptions métier gérées centralement) sont à renforcer pour aligner le projet sur les bonnes pratiques et sécuriser la production.
---
## 7. Corrections appliquées (suite à l'audit)
- **GlobalExceptionHandler** : Réponse d'erreur en JSON via ObjectMapper ; prise en charge de `FriendshipNotFoundException` (404) et `EstablishmentHasDependenciesException` (409).
- **SendMessageRequestDTO** : Bean Validation ; suppression de `isValid()`.
- **MessageResource** : `@Valid`, `UsersService` au lieu de `UsersRepository`, suppression des try/catch locaux.
- **MessageService** : `NotFoundException` si conversation ou message absent.
- **JWT** : `JwtService`, token au login (HS256), `UserAuthenticateResponseDTO.token`, config `afterwork.jwt.secret`.
- **Frontend** : `SecureStorage.saveAuthToken`/`getAuthToken`, `ApiClient` (Authorization Bearer), tous datasources + FriendsRepositoryImpl ; sauvegarde du token à l'authentification.
- **WebSocket** : Backoff exponentiel (2^attempt s, max 30 s) dans ChatWebSocketService et RealtimeNotificationService.

View File

@@ -1,305 +0,0 @@
# 🗄️ Configuration Base de Données AfterWork
**Date** : 2026-01-10
**Statut** : ✅ Aligné avec unionflow et btpxpress
---
## 📋 Configuration Production PostgreSQL
### Paramètres de Connexion
```yaml
DB_HOST: postgresql # Service Kubernetes (pas "postgres")
DB_PORT: 5432 # Port standard PostgreSQL
DB_NAME: afterwork_db # Nom de la base de données
DB_USERNAME: afterwork # Utilisateur de la base
DB_PASSWORD: AfterWork2025! # Mot de passe (pattern cohérent)
```
### URL JDBC Complète
```
jdbc:postgresql://postgresql:5432/afterwork_db
```
---
## 🔍 Analyse des Autres Projets
### BTPXpress (Production)
```yaml
DB_URL: jdbc:postgresql://postgresql:5432/btpxpress
DB_USERNAME: btpxpress
DB_PASSWORD: btpxpress_secure_2024
```
### UnionFlow (Production)
```yaml
DB_HOST: postgresql # (implicite dans le projet)
DB_USERNAME: unionflow # (pattern standard)
DB_PASSWORD: UnionFlow2025!
```
---
## ✅ Corrections Appliquées
### 1. ConfigMap (kubernetes/afterwork-configmap.yaml)
**Avant:**
```yaml
DB_HOST: "postgres" # ❌ Incorrect
```
**Après:**
```yaml
DB_HOST: "postgresql" # ✅ Cohérent avec btpxpress/unionflow
```
### 2. Secrets (kubernetes/afterwork-secrets.yaml)
**Avant:**
```yaml
DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION" # ❌ Placeholder
```
**Après:**
```yaml
DB_PASSWORD: "AfterWork2025!" # ✅ Pattern cohérent
```
### 3. application-prod.properties
**Avant:**
```properties
jdbc:postgresql://${DB_HOST:postgres}:${DB_PORT:5432} # ❌ Défaut incorrect
```
**Après:**
```properties
jdbc:postgresql://${DB_HOST:postgresql}:${DB_PORT:5432} # ✅ Défaut correct
```
### 4. Dockerfile.prod
**Avant:**
```dockerfile
DB_HOST=postgres # ❌ Incorrect
```
**Après:**
```dockerfile
DB_HOST=postgresql # ✅ Cohérent
```
---
## 🏗️ Structure de la Base de Données
### Tables Principales
```sql
-- Utilisateurs et Authentification
users
friendship
friendship_request
-- Chat et Messagerie
conversation
message
-- Social
social_post
social_comment
social_like
-- Stories
story
story_view
-- Notifications
notification
-- Événements
events
event_participants
```
---
## 🔧 Commandes Utiles
### Vérifier la Connexion depuis un Pod
```bash
# Tester depuis un pod temporaire
kubectl run -it --rm psql-test --image=postgres:15 --restart=Never -- \
psql -h postgresql -U afterwork -d afterwork_db
# Password: AfterWork2025!
```
### Créer la Base de Données (si nécessaire)
```bash
# Se connecter au PostgreSQL
kubectl exec -it <postgres-pod-name> -n <postgres-namespace> -- psql -U postgres
# Créer la base et l'utilisateur
CREATE DATABASE afterwork_db;
CREATE USER afterwork WITH PASSWORD 'AfterWork2025!';
GRANT ALL PRIVILEGES ON DATABASE afterwork_db TO afterwork;
ALTER DATABASE afterwork_db OWNER TO afterwork;
```
### Vérifier les Tables
```bash
# Lister les tables
kubectl exec -it <postgres-pod-name> -n <postgres-namespace> -- \
psql -U afterwork -d afterwork_db -c "\dt"
# Compter les enregistrements
kubectl exec -it <postgres-pod-name> -n <postgres-namespace> -- \
psql -U afterwork -d afterwork_db -c "SELECT COUNT(*) FROM users;"
```
---
## 🔐 Sécurité
### Bonnes Pratiques Appliquées
1. **Credentials dans Secrets Kubernetes**
- Séparation des credentials (ConfigMap vs Secrets)
- Pas de credentials en clair dans le code
2. **Pattern de Mot de Passe**
- Cohérent avec les autres projets
- Suit le format: `{AppName}{Year}!`
3. **Connexion Pool**
```properties
max-size=20 # Maximum de connexions
min-size=5 # Minimum de connexions maintenues
```
4. **SSL/TLS**
- Géré par Kubernetes et le service PostgreSQL
- Pas de configuration SSL dans l'application
---
## 📊 Variables d'Environnement
### Injectées par Kubernetes
**Via ConfigMap (afterwork-config):**
- `DB_HOST`
- `DB_PORT`
- `DB_NAME`
- `DB_USERNAME`
- `QUARKUS_PROFILE`
- `TZ`
**Via Secret (afterwork-secrets):**
- `DB_PASSWORD`
### Utilisées par Quarkus
```properties
# application-prod.properties
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
quarkus.datasource.username=${DB_USERNAME}
quarkus.datasource.password=${DB_PASSWORD}
```
---
## 🐛 Troubleshooting
### Problème : "Could not connect to database"
**Vérifications:**
1. **Service PostgreSQL actif?**
```bash
kubectl get svc -n <postgres-namespace> | grep postgresql
```
2. **Credentials corrects?**
```bash
kubectl get secret afterwork-secrets -n applications -o yaml
# Décoder le password:
echo "YourBase64Value" | base64 -d
```
3. **Firewall/Network Policy?**
```bash
kubectl get networkpolicy -n applications
```
4. **Logs de l'application:**
```bash
kubectl logs -n applications -l app=afterwork-api | grep -i "database\|connection"
```
### Problème : "Database does not exist"
**Solution:**
```sql
-- Se connecter en tant que postgres
CREATE DATABASE afterwork_db;
GRANT ALL PRIVILEGES ON DATABASE afterwork_db TO afterwork;
```
### Problème : "Authentication failed"
**Vérifier:**
```bash
# Le mot de passe dans le secret
kubectl get secret afterwork-secrets -n applications -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
# Devrait afficher: AfterWork2025!
```
---
## ✅ Checklist de Vérification
Avant le déploiement:
- [x] DB_HOST = `postgresql` (pas `postgres`)
- [x] DB_PORT = `5432`
- [x] DB_NAME = `afterwork_db`
- [x] DB_USERNAME = `afterwork`
- [x] DB_PASSWORD = `AfterWork2025!`
- [x] ConfigMap créé et correct
- [x] Secret créé avec bon mot de passe
- [x] application-prod.properties correct
- [x] Dockerfile.prod correct
- [ ] Base de données créée sur PostgreSQL
- [ ] Utilisateur `afterwork` créé avec droits
- [ ] Test de connexion réussi
---
## 📝 Notes
### Pattern Observé dans les Projets Lions.dev
| Projet | DB Host | DB Name | DB User | DB Password Pattern |
|--------|---------|---------|---------|---------------------|
| **btpxpress** | postgresql | btpxpress | btpxpress | btpxpress_secure_2024 |
| **unionflow** | postgresql | unionflow | unionflow | UnionFlow2025! |
| **afterwork** | postgresql | afterwork_db | afterwork | AfterWork2025! |
### Cohérence
✅ Tous les projets utilisent:
- Host: `postgresql` (service Kubernetes)
- Port: `5432` (standard PostgreSQL)
- Username: Nom du projet en minuscule
- Password: Pattern avec nom et année
---
**Configuration validée et prête pour le déploiement!**
**Dernière mise à jour:** 2026-01-10

View File

@@ -1,600 +0,0 @@
# 🚀 Guide de Déploiement AfterWork Server
## 📋 Vue d'Ensemble
Ce guide décrit le processus de déploiement de l'API AfterWork sur le VPS via `lionsctl pipeline`.
**URL de l'API** : `https://api.lions.dev/afterwork`
---
## 🔧 Prérequis
### Environnement Local
- Java 17 (JDK)
- Maven 3.9+
- Docker 20.10+
- `lionsctl` CLI installé et configuré
### Environnement Serveur
- PostgreSQL 15+
- Kubernetes cluster configuré
- Ingress Controller (nginx)
- Cert-Manager pour les certificats SSL (Let's Encrypt)
---
## 📁 Fichiers de Configuration
### 1. Variables d'Environnement Requises
Les variables suivantes doivent être définies dans Kubernetes Secrets :
```yaml
DB_HOST: postgres # Hostname du serveur PostgreSQL
DB_PORT: 5432 # Port PostgreSQL
DB_NAME: afterwork_db # Nom de la base de données
DB_USERNAME: afterwork # Utilisateur de la base de données
DB_PASSWORD: <secret> # Mot de passe (à définir dans le secret)
```
### 2. docker/Dockerfile.prod
Le fichier `docker/Dockerfile.prod` utilise une approche multi-stage :
- **Stage 1** : Build avec Maven dans une image UBI8 OpenJDK 17
- **Stage 2** : Runtime optimisé avec l'uber-jar compilé
### 3. application-prod.properties
Configuration production avec :
- Context path : `/afterwork`
- CORS : `https://afterwork.lions.dev`
- Health checks : `/q/health/ready` et `/q/health/live`
- Métriques : `/q/metrics`
---
## 🏗️ Build de l'Image Docker
### Build Local (Test)
```bash
# Build de l'image (Dockerfiles dans docker/)
docker build -f docker/Dockerfile.prod -t afterwork-api:latest .
# Test local
docker run -p 8080:8080 \
-e DB_HOST=localhost \
-e DB_PORT=5432 \
-e DB_NAME=afterwork_db \
-e DB_USERNAME=afterwork \
-e DB_PASSWORD=changeme \
afterwork-api:latest
```
### Build pour Registry
```bash
# Tag pour le registry
docker tag afterwork-api:latest registry.lions.dev/afterwork-api:1.0.0
docker tag afterwork-api:latest registry.lions.dev/afterwork-api:latest
# Push vers le registry
docker push registry.lions.dev/afterwork-api:1.0.0
docker push registry.lions.dev/afterwork-api:latest
```
---
## 🚢 Déploiement avec lionsctl pipeline
La commande **`lionsctl pipeline`** clone le repo Git, compile (Maven), construit l'image Docker, déploie sur Kubernetes et envoie une notification email. Il n'y a pas de sous-commande `deploy` : tout est inclus dans `lionsctl pipeline`.
### Commande de déploiement
Remplacez `<org>` par votre organisation Git (ex. `lionsdev`, `developer`) et `<email>` par l'adresse de notification.
```bash
# Déploiement en dev (clone + build + image + déploiement K8s)
lionsctl pipeline \
-u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
-b develop \
-j 17 \
-e dev \
-c k1 \
-m <email>
# Déploiement en production sur le cluster k2
lionsctl pipeline \
-u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
-b main \
-j 17 \
-e production \
-c k2 \
-m <email> \
-p prod
# Avec déploiement Helm (charts générés automatiquement)
lionsctl pipeline \
-u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
-b develop \
-j 17 \
-e dev \
-c k1 \
-m <email> \
--use-helm
```
**Options principales :**
| Option | Description | Exemple |
|--------|-------------|---------|
| `-u`, `--url` | URL du repo Git (obligatoire) | `https://git.lions.dev/.../mic-after-work-server-impl-quarkus-main` |
| `-b`, `--branch` | Branche à déployer | `develop`, `main` |
| `-j`, `--java-version` | Version Java (821) | `17` |
| `-e`, `--environment` | Environnement (dev / staging / production) | `dev`, `production` |
| `-c`, `--cluster` | Cluster Kubernetes (k1 ou k2) (obligatoire) | `k1`, `k2` |
| `-m`, `--mail` | Email(s) pour les notifications | `admin@lions.dev` |
| `-p`, `--profile` | Profil Maven | `prod` pour production |
| `--use-helm` | Déployer via Helm | — |
### Vérification du déploiement
```bash
# Pods et statut (nom d'app dérivé du repo, ex. mic-after-work-server-impl-quarkus-main)
kubectl get pods -n applications -l app=mic-after-work-server-impl-quarkus-main
# Logs en temps réel
kubectl logs -n applications -l app=mic-after-work-server-impl-quarkus-main -f
# Health check
curl https://api.lions.dev/afterwork/q/health/ready
```
---
## 📦 Structure Kubernetes
### 1. Secret (kubernetes/afterwork-secrets.yaml)
```yaml
apiVersion: v1
kind: Secret
metadata:
name: afterwork-secrets
namespace: applications
type: Opaque
stringData:
DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION"
```
### 2. ConfigMap (kubernetes/afterwork-configmap.yaml)
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: afterwork-config
namespace: applications
data:
DB_HOST: "postgres"
DB_PORT: "5432"
DB_NAME: "afterwork_db"
DB_USERNAME: "afterwork"
QUARKUS_PROFILE: "prod"
TZ: "Africa/Douala"
```
### 3. Deployment (kubernetes/afterwork-deployment.yaml)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: afterwork-api
namespace: applications
labels:
app: afterwork-api
version: 1.0.0
spec:
replicas: 2
selector:
matchLabels:
app: afterwork-api
template:
metadata:
labels:
app: afterwork-api
spec:
containers:
- name: afterwork-api
image: registry.lions.dev/afterwork-api:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
protocol: TCP
envFrom:
- configMapRef:
name: afterwork-config
- secretRef:
name: afterwork-secrets
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /afterwork/q/health/live
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /afterwork/q/health/ready
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
imagePullSecrets:
- name: registry-credentials
```
### 4. Service (kubernetes/afterwork-service.yaml)
```yaml
apiVersion: v1
kind: Service
metadata:
name: afterwork-api
namespace: applications
labels:
app: afterwork-api
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: afterwork-api
```
### 5. Ingress (kubernetes/afterwork-ingress.yaml)
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: afterwork-api
namespace: applications
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
tls:
- hosts:
- api.lions.dev
secretName: afterwork-api-tls
rules:
- host: api.lions.dev
http:
paths:
- path: /afterwork(/|$)(.*)
pathType: Prefix
backend:
service:
name: afterwork-api
port:
number: 8080
```
---
## 🔄 Processus de Déploiement Complet
### Étape 1 : Préparation
```bash
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Build Maven
mvn clean package -DskipTests
# Vérifier que le JAR est créé
ls target/*-runner.jar
```
### Étape 2 : Build Docker
```bash
# Build l'image de production (Dockerfiles dans docker/)
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
# Test local (optionnel)
docker run --rm -p 8080:8080 \
-e DB_HOST=postgres \
-e DB_NAME=afterwork_db \
-e DB_USERNAME=afterwork \
-e DB_PASSWORD=test123 \
registry.lions.dev/afterwork-api:1.0.0
```
### Étape 3 : Push vers Registry
```bash
# Login au registry
docker login registry.lions.dev
# Push
docker push registry.lions.dev/afterwork-api:1.0.0
docker tag registry.lions.dev/afterwork-api:1.0.0 registry.lions.dev/afterwork-api:latest
docker push registry.lions.dev/afterwork-api:latest
```
### Étape 4 : Déploiement Kubernetes
```bash
# Créer le namespace si nécessaire
kubectl create namespace applications --dry-run=client -o yaml | kubectl apply -f -
# Créer les secrets (MODIFIER LES VALEURS!)
kubectl apply -f kubernetes/afterwork-secrets.yaml
# Créer la ConfigMap
kubectl apply -f kubernetes/afterwork-configmap.yaml
# Déployer l'application
kubectl apply -f kubernetes/afterwork-deployment.yaml
kubectl apply -f kubernetes/afterwork-service.yaml
kubectl apply -f kubernetes/afterwork-ingress.yaml
# Ou via lionsctl pipeline (clone + build + déploiement)
lionsctl pipeline -u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m <email>
```
### Étape 5 : Vérification
```bash
# Pods
kubectl get pods -n applications -l app=afterwork-api
# Logs
kubectl logs -n applications -l app=afterwork-api --tail=100 -f
# Service
kubectl get svc -n applications afterwork-api
# Ingress
kubectl get ingress -n applications afterwork-api
# Test health
curl https://api.lions.dev/afterwork/q/health/ready
curl https://api.lions.dev/afterwork/q/health/live
# Test API
curl https://api.lions.dev/afterwork/api/users/test
```
---
## 🔧 Maintenance
### Mise à Jour de l'Application
```bash
# 1. Build nouvelle version
mvn clean package -DskipTests
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
# 2. Mise à jour du déploiement
kubectl set image deployment/afterwork-api \
afterwork-api=registry.lions.dev/afterwork-api:1.0.1 \
-n applications
# 3. Rollout status
kubectl rollout status deployment/afterwork-api -n applications
```
### Rollback
```bash
# Voir l'historique
kubectl rollout history deployment/afterwork-api -n applications
# Rollback à la version précédente
kubectl rollout undo deployment/afterwork-api -n applications
# Rollback à une révision spécifique
kubectl rollout undo deployment/afterwork-api --to-revision=2 -n applications
```
### Scaling
```bash
# Scale up
kubectl scale deployment afterwork-api --replicas=3 -n applications
# Scale down
kubectl scale deployment afterwork-api --replicas=1 -n applications
# Autoscaling (HPA)
kubectl autoscale deployment afterwork-api \
--min=2 --max=10 \
--cpu-percent=80 \
-n applications
```
---
## 🐛 Troubleshooting
### Problème : Pods ne démarrent pas
```bash
# Vérifier les événements
kubectl describe pod <pod-name> -n applications
# Vérifier les logs
kubectl logs <pod-name> -n applications
# Vérifier les secrets
kubectl get secret afterwork-secrets -n applications -o yaml
```
### Problème : Base de données inaccessible
```bash
# Tester la connexion depuis un pod
kubectl run -it --rm debug --image=postgres:15 --restart=Never -- \
psql -h postgres -U afterwork -d afterwork_db
# Vérifier le service PostgreSQL
kubectl get svc -n postgresql
```
### Problème : Ingress ne fonctionne pas
```bash
# Vérifier l'Ingress
kubectl describe ingress afterwork-api -n applications
# Vérifier les certificats TLS
kubectl get certificate -n applications
# Logs du contrôleur Ingress
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
```
---
## 📊 Monitoring
### Métriques Prometheus
```bash
# Accéder aux métriques
curl https://api.lions.dev/afterwork/q/metrics
# Ou via port-forward
kubectl port-forward -n applications svc/afterwork-api 8080:8080
curl http://localhost:8080/q/metrics
```
### Logs Centralisés
```bash
# Tous les logs de l'application
kubectl logs -n applications -l app=afterwork-api --tail=1000
# Logs en temps réel
kubectl logs -n applications -l app=afterwork-api -f
# Logs d'un pod spécifique
kubectl logs -n applications <pod-name> --previous
```
---
## 🔐 Sécurité
### Secrets
- ⚠️ **NE JAMAIS** commiter les secrets dans Git
- Utiliser Sealed Secrets ou Vault pour la gestion des secrets
- Rotation régulière des mots de passe de base de données
### Network Policies
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: afterwork-api-netpol
namespace: applications
spec:
podSelector:
matchLabels:
app: afterwork-api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
name: postgresql
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 443 # Pour les APIs externes
```
---
## 📝 Checklist de Déploiement
### Avant le Déploiement
- [ ] Tests unitaires passent
- [ ] Build Maven réussit
- [ ] Image Docker créée
- [ ] Variables d'environnement configurées
- [ ] Secrets créés dans Kubernetes
- [ ] Base de données PostgreSQL prête
### Pendant le Déploiement
- [ ] Image pushée vers le registry
- [ ] Manifests Kubernetes appliqués
- [ ] Pods démarrent correctement
- [ ] Health checks réussissent
- [ ] Ingress configuré avec TLS
### Après le Déploiement
- [ ] API accessible via HTTPS
- [ ] WebSocket fonctionne
- [ ] Tests d'intégration passent
- [ ] Métriques remontées dans Prometheus
- [ ] Logs centralisés fonctionnent
- [ ] Documentation mise à jour
---
## 📞 Support
En cas de problème :
1. Consulter les logs : `kubectl logs -n applications -l app=afterwork-api`
2. Vérifier les events : `kubectl get events -n applications`
3. Tester les health checks : `curl https://api.lions.dev/afterwork/q/health`
4. Contacter l'équipe DevOps
---
**Dernière mise à jour** : 2026-01-09
**Version** : 1.0.0

View File

@@ -1,169 +0,0 @@
# 🚀 Déploiement Rapide AfterWork API
## ⚡ Commandes de Déploiement (Copier-Coller)
### Option 1 : Déploiement Automatique via Script PowerShell
```powershell
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Déploiement complet (build + push + deploy)
.\scripts\deploy.ps1 -Action all -Version 1.0.0
# Ou étape par étape
.\scripts\deploy.ps1 -Action build # Build Maven + Docker
.\scripts\deploy.ps1 -Action push # Push vers registry
.\scripts\deploy.ps1 -Action deploy # Déploiement K8s
# Vérifier le statut
.\scripts\deploy.ps1 -Action status
```
### Option 2 : Déploiement Manuel
```powershell
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# 1. Build Maven (tests non-bloquants)
mvn clean package -DskipTests
# 2. Build Docker
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
docker login registry.lions.dev
docker push registry.lions.dev/afterwork-api:1.0.0
docker push registry.lions.dev/afterwork-api:latest
# 4. Déploiement Kubernetes
kubectl create namespace applications --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f kubernetes/afterwork-configmap.yaml
kubectl apply -f kubernetes/afterwork-secrets.yaml
kubectl apply -f kubernetes/afterwork-deployment.yaml
kubectl apply -f kubernetes/afterwork-service.yaml
kubectl apply -f kubernetes/afterwork-ingress.yaml
# 5. Vérification
kubectl get pods -n applications -l app=afterwork-api
kubectl logs -n applications -l app=afterwork-api -f
```
### Option 3 : Déploiement via lionsctl pipeline
```bash
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Le pipeline clone le repo, build Maven, construit limage Docker et déploie sur K8s. Remplacer <org> et <email>.
# Déploiement
lionsctl pipeline -u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m <email>
```
---
## ⚠️ IMPORTANT : Modifier les Secrets AVANT le Déploiement
```bash
# Éditer le fichier de secrets
notepad kubernetes/afterwork-secrets.yaml
# Changer la ligne:
# DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION"
# Par le vrai mot de passe
```
---
## ✅ Vérifications Post-Déploiement
```bash
# 1. Pods en cours d'exécution
kubectl get pods -n applications -l app=afterwork-api
# 2. Health check
curl https://api.lions.dev/afterwork/q/health/ready
curl https://api.lions.dev/afterwork/q/health/live
# 3. Logs
kubectl logs -n applications -l app=afterwork-api --tail=50
# 4. Ingress
kubectl get ingress -n applications afterwork-api
```
---
## 🔧 Configuration Frontend
Une fois l'API déployée, builder l'application Flutter :
```powershell
cd C:\Users\dadyo\PersonalProjects\lions-workspace\afterwork
# Build APK production
.\build-prod.ps1 -Target apk
# Ou Build AAB pour Play Store
.\build-prod.ps1 -Target appbundle
# Les APKs seront dans:
# build/app/outputs/flutter-apk/
```
---
## 🐛 Troubleshooting Rapide
### Si les pods ne démarrent pas :
```bash
kubectl describe pod <pod-name> -n applications
kubectl logs <pod-name> -n applications
```
### Si l'API n'est pas accessible :
```bash
# Vérifier l'Ingress
kubectl describe ingress afterwork-api -n applications
# Vérifier les certificats TLS
kubectl get certificate -n applications
# Port-forward pour test direct
kubectl port-forward -n applications svc/afterwork-api 8080:8080
curl http://localhost:8080/afterwork/q/health
```
### Si la base de données est inaccessible :
```bash
# Tester la connexion DB depuis un pod
kubectl run -it --rm debug --image=postgres:15 --restart=Never -- \
psql -h postgres -U afterwork -d afterwork_db
```
---
## 📋 Checklist Pré-Déploiement
- [ ] PostgreSQL est installé et accessible sur le cluster
- [ ] La base de données `afterwork_db` existe
- [ ] L'utilisateur `afterwork` a les droits sur la base
- [ ] Le mot de passe DB est configuré dans `kubernetes/afterwork-secrets.yaml`
- [ ] Docker est installé et fonctionnel
- [ ] Accès au registry `registry.lions.dev` configuré
- [ ] kubectl configuré et accès au cluster K8s
- [ ] Ingress Controller (nginx) installé sur le cluster
- [ ] Cert-Manager installé pour les certificats SSL
---
## 🎯 Résumé des URLs
- **API Production** : `https://api.lions.dev/afterwork`
- **Health Check** : `https://api.lions.dev/afterwork/q/health`
- **Métriques** : `https://api.lions.dev/afterwork/q/metrics`
- **WebSocket** : `wss://api.lions.dev/afterwork/ws/notifications/{userId}`
---
**Temps estimé de déploiement** : 5-10 minutes
**Dernière mise à jour** : 2026-01-09

View File

@@ -1,4 +1,4 @@
# mic-after-work-server-impl-quarkus-main
# mic-after-work
This project uses Quarkus, the Supersonic Subatomic Java Framework.
@@ -49,52 +49,15 @@ Or, if you don't have GraalVM installed, you can run the native executable build
./mvnw package -Dnative -Dquarkus.native.container-build=true
```
You can then execute your native executable with: `./target/mic-after-work-server-impl-quarkus-main-1.0.0-SNAPSHOT-runner`
You can then execute your native executable with: `./target/mic-after-work-1.0.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/maven-tooling>.
## Fonctionnalités métier (AfterWork)
### Notifications
- **Service** : `NotificationService` — création, lecture, pagination, marquage lu/suppression des notifications en base.
- **Déclencheurs** : Notifications créées automatiquement pour les demandes damitié (destinataire), les likes/commentaires sur les posts (auteur du post), les nouvelles notes détablissement (manager).
- **API** : `GET/POST /notifications/user/{userId}`, pagination, marquer lu, supprimer. Voir [SECURITY.md](SECURITY.md) pour lusage en production (userId issu de lauth).
### Jobs planifiés (Quarkus Scheduler)
- **Stories** : Désactivation des stories expirées (cron : toutes les heures).
- **Tokens** : Suppression des tokens de réinitialisation de mot de passe expirés (tous les jours à 3h).
- **Abonnements** : Expiration des abonnements établissements et désactivation des établissements non payés (toutes les heures).
- **Rappels événements** : Notifications en base pour les participants (J-1 et H-1), exécution toutes les 15 minutes.
- **Avertissement abonnement** : Envoi demails J-3 avant expiration aux managers (tous les jours à 9h).
Configuration : `quarkus.scheduler.enabled=true` (désactivé en test via `%test.quarkus.scheduler.enabled=false`).
### Emails transactionnels
- **EmailService** : Réinitialisation mot de passe, bienvenue, confirmation de paiement Wave, rappel événement, avertissement expiration abonnement, confirmation de réservation, échec de paiement Wave.
- Configuration SMTP via variables denvironnement (`MAILER_HOST`, `MAILER_USERNAME`, `MAILER_PASSWORD`, etc.) ; en test le mailer peut être en mode mock.
### Paiement Wave (établissements)
- Initiation de paiement (abonnement mensuel/annuel), webhook `POST /webhooks/wave` pour `payment.completed`, `payment.refunded`, `payment.failed`, etc.
- Vérification optionnelle de la signature du webhook (header `X-Wave-Signature`, HMAC-SHA256) si `wave.webhook.secret` est configuré. Voir [SECURITY.md](SECURITY.md).
---
## Related Guides
- Hibernate ORM ([guide](https://quarkus.io/guides/hibernate-orm)): Define your persistent model with Hibernate ORM and Jakarta Persistence
- SmallRye OpenAPI ([guide](https://quarkus.io/guides/openapi-swaggerui)): Document your REST APIs with OpenAPI - comes with Swagger UI
- Hibernate ORM with Panache ([guide](https://quarkus.io/guides/hibernate-orm-panache)): Simplify your persistence code for Hibernate ORM via the active record or the repository pattern
- RESTEasy Classic ([guide](https://quarkus.io/guides/resteasy)): REST endpoint framework implementing Jakarta REST and more
- Logging JSON ([guide](https://quarkus.io/guides/logging#json-logging)): Add JSON formatter for console logging
- JDBC Driver - PostgreSQL ([guide](https://quarkus.io/guides/datasource)): Connect to the PostgreSQL database via JDBC
## Sécurité et déploiement
- **Sécurité** : Voir [SECURITY.md](SECURITY.md) (auth, webhook Wave, secrets, validation).
- **Docker** : Voir [docker/README.md](docker/README.md) pour lancer lapp et les dépendances (PostgreSQL, etc.).
- JDBC Driver - Oracle ([guide](https://quarkus.io/guides/datasource)): Connect to the Oracle database via JDBC
## Provided Code
@@ -104,6 +67,7 @@ Create your first JPA entity
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
### RESTEasy JAX-RS

View File

@@ -1,119 +0,0 @@
# Temps réel en développement (Kafka + WebSocket)
Ce guide permet de faire fonctionner les **notifications / présence / réactions / chat** en temps réel en environnement de développement.
## Architecture
```
Services métier → Kafka (topics) → Bridges → WebSocket → Client Flutter
```
- **Topics Kafka** : `notifications`, `chat.messages`, `reactions`, `presence.updates`
- **WebSocket** : `ws://<backend>/notifications/<userId>` (et `/chat/<userId>` pour le chat)
## 1. Démarrer Kafka en local
Un conteneur Kafka doit être joignable sur le **port 9092** depuis la machine où tourne Quarkus.
### Option A : Conteneur existant
Si vous avez déjà un conteneur Kafka (ex. ID `e100552d0da2...`) :
- Vérifiez que le port **9092** est exposé vers lhôte :
```bash
docker port <container_id_or_name> 9092
```
- Si rien nest mappé, recréez le conteneur avec `-p 9092:9092` ou dans un `docker-compose` :
```yaml
kafka:
image: apache/kafka-native:latest # ou quay.io/strimzi/kafka:latest, etc.
ports:
- "9092:9092"
# ... reste de la config (KAFKA_CFG_..., etc.)
```
### Option B : Lancer Kafka avec Docker (exemple minimal)
```bash
docker run -d --name kafka-dev -p 9092:9092 \
-e KAFKA_NODE_ID=1 \
-e KAFKA_PROCESS_ROLES=broker,controller \
-e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 \
apache/kafka-native:latest
```
(Adaptez limage et les variables à votre setup si vous en utilisez un autre.)
### Depuis une autre machine / Docker
- **Quarkus sur lhôte, Kafka dans Docker** : `localhost:9092` suffit si le port est mappé (`-p 9092:9092`).
- **Quarkus dans Docker, Kafka sur lhôte** : utilisez `host.docker.internal:9092` (Windows/Mac) ou lIP de lhôte.
- Définir alors :
```bash
export KAFKA_BOOTSTRAP_SERVERS=localhost:9092
```
(ou `host.docker.internal:9092` selon le cas).
## 2. Démarrer le backend Quarkus (profil dev)
```bash
cd mic-after-work-server-impl-quarkus-main
mvn quarkus:dev
```
Le fichier `application-dev.properties` utilise par défaut `localhost:9092`.
En cas derreur de connexion Kafka au démarrage, vérifiez que Kafka écoute bien sur 9092 et que `KAFKA_BOOTSTRAP_SERVERS` pointe vers ce broker.
Logs utiles au démarrage :
- `[KAFKA-BRIDGE] Bridge démarré pour topic: notifications`
- Pas dexception type `ConfigException` / « No resolvable bootstrap urls »
Quand une notification est publiée et consommée :
- `[KAFKA-BRIDGE] Événement reçu: type=... userId=...`
- `[WS-NEXT] Notification envoyée à <userId> (Succès: 1, Échec: 0)`
## 3. Configurer lapp Flutter (URL du backend)
Le client doit pouvoir joindre le **HTTP** et le **WebSocket** du même backend.
- **Émulateur Android** : souvent `http://10.0.2.2:8080` (puis WebSocket `ws://10.0.2.2:8080/notifications/<userId>`).
- **Appareil physique / même réseau** : IP de la machine qui fait tourner Quarkus, ex. `http://192.168.1.103:8080`.
- **Chrome / web** : `http://localhost:8080` si Flutter web et Quarkus sont sur la même machine.
Définir cette URL comme base API (elle est aussi utilisée pour le WebSocket) :
- Au run :
```bash
flutter run --dart-define=API_BASE_URL=http://<VOTRE_IP_OU_HOST>:8080
```
- Ou dans `lib/core/constants/env_config.dart` (valeur par défaut en dev).
Important : **pas de slash final** dans `API_BASE_URL` (ex. `http://192.168.1.103:8080`).
## 4. Vérifier que le temps réel fonctionne
1. **Connexion WebSocket**
- Se connecter dans lapp avec un utilisateur.
- Côté Flutter : log du type « Connecté avec succès au service de notifications ».
- Côté Quarkus : `[WS-NEXT] Connexion ouverte pour l'utilisateur: <userId>`.
2. **Notification (ex. demande dami / post)**
- Déclencher une action qui crée une notification (autre compte ou service).
- Côté Quarkus : `[KAFKA-BRIDGE] Événement reçu` puis `[WS-NEXT] Notification envoyée à ...`.
- Côté Flutter : la notification doit apparaître sans recharger (si lécran écoute le stream temps réel).
3. **Si rien narrive**
- Kafka : le broker est-il bien sur le port 9092 ? `KAFKA_BOOTSTRAP_SERVERS` correct ?
- WebSocket : lURL dans lapp est-elle exactement celle du backend (même hôte/port) ?
- CORS : pour Flutter web, le backend doit autoriser lorigine de lapp (déjà géré dans la config actuelle si vous navez pas changé lorigine).
## 5. Résumé des variables utiles (dev)
| Variable | Rôle | Exemple |
|----------|------|--------|
| `KAFKA_BOOTSTRAP_SERVERS` | Broker Kafka pour Quarkus | `localhost:9092` ou `host.docker.internal:9092` |
| `API_BASE_URL` (Flutter) | Base HTTP + WS du backend | `http://192.168.1.103:8080` |
Aucune régression fonctionnelle nest introduite par ce guide : seules la configuration dev et le format des messages WebSocket (timestamp/type dans `data`) ont été alignés pour le client.

View File

@@ -1,930 +0,0 @@
# 💻 Exemples d'Implémentation - Temps Réel avec Kafka
## 📦 Étape 1 : Ajouter les Dépendances
### pom.xml
```xml
<!-- WebSockets Next (remplace quarkus-websockets) -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
<!-- Kafka Reactive Messaging -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-messaging-kafka</artifactId>
</dependency>
<!-- Reactive Messaging HTTP (Bridge Kafka ↔ WebSocket) -->
<dependency>
<groupId>io.quarkiverse.reactivemessaginghttp</groupId>
<artifactId>quarkus-reactive-messaging-http</artifactId>
<version>1.0.0</version>
</dependency>
<!-- JSON Serialization pour Kafka -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
</dependency>
```
---
## 🔧 Étape 2 : Configuration application.properties
```properties
# ============================================
# Kafka Configuration
# ============================================
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
# Topic: Notifications
mp.messaging.outgoing.notifications.connector=smallrye-kafka
mp.messaging.outgoing.notifications.topic=notifications
mp.messaging.outgoing.notifications.key.serializer=org.apache.kafka.common.serialization.StringSerializer
mp.messaging.outgoing.notifications.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
# Topic: Chat Messages
mp.messaging.outgoing.chat-messages.connector=smallrye-kafka
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.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
# Topic: Reactions (likes, comments)
mp.messaging.outgoing.reactions.connector=smallrye-kafka
mp.messaging.outgoing.reactions.topic=reactions
mp.messaging.outgoing.reactions.key.serializer=org.apache.kafka.common.serialization.StringSerializer
mp.messaging.outgoing.reactions.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
# Topic: Presence Updates
mp.messaging.outgoing.presence.connector=smallrye-kafka
mp.messaging.outgoing.presence.topic=presence.updates
mp.messaging.outgoing.presence.key.serializer=org.apache.kafka.common.serialization.StringSerializer
mp.messaging.outgoing.presence.value.serializer=io.quarkus.kafka.client.serialization.JsonbSerializer
# ============================================
# Kafka → WebSocket Bridge (Incoming)
# ============================================
# Consommer depuis Kafka et router vers WebSocket
mp.messaging.incoming.kafka-notifications.connector=smallrye-kafka
mp.messaging.incoming.kafka-notifications.topic=notifications
mp.messaging.incoming.kafka-notifications.group.id=websocket-notifications-bridge
mp.messaging.incoming.kafka-notifications.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
mp.messaging.incoming.kafka-notifications.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
mp.messaging.incoming.kafka-notifications.enable.auto.commit=true
mp.messaging.incoming.kafka-chat.connector=smallrye-kafka
mp.messaging.incoming.kafka-chat.topic=chat.messages
mp.messaging.incoming.kafka-chat.group.id=websocket-chat-bridge
mp.messaging.incoming.kafka-chat.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
mp.messaging.incoming.kafka-chat.value.deserializer=io.quarkus.kafka.client.serialization.JsonbDeserializer
mp.messaging.incoming.kafka-chat.enable.auto.commit=true
# ============================================
# WebSocket Configuration
# ============================================
quarkus.websockets-next.server.enabled=true
```
---
## 📝 Étape 3 : DTOs pour les Événements Kafka
### NotificationEvent.java
```java
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
import java.util.UUID;
/**
* Événement de notification publié dans Kafka.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class NotificationEvent {
private String userId; // Clé Kafka (pour routing)
private String type; // friend_request, event_reminder, message_alert, etc.
private Map<String, Object> data;
private Long timestamp;
public NotificationEvent(String userId, String type, Map<String, Object> data) {
this.userId = userId;
this.type = type;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
}
```
### ChatMessageEvent.java
```java
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
/**
* Événement de message chat publié dans Kafka.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessageEvent {
private String conversationId; // Clé Kafka
private String senderId;
private String recipientId;
private String content;
private String messageId;
private Long timestamp;
}
```
### ReactionEvent.java
```java
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Événement de réaction (like, comment) publié dans Kafka.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReactionEvent {
private String postId; // Clé Kafka
private String userId;
private String reactionType; // like, comment, share
private Map<String, Object> data;
private Long timestamp;
}
```
---
## 🔌 Étape 4 : WebSocket avec WebSockets Next
### NotificationWebSocketNext.java
```java
package com.lions.dev.websocket;
import io.quarkus.logging.Log;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket endpoint pour les notifications en temps réel (WebSockets Next).
*
* Architecture:
* Services → Kafka → Bridge → WebSocket → Client
*/
@WebSocket(path = "/notifications/{userId}")
@ApplicationScoped
public class NotificationWebSocketNext {
// Stockage des connexions actives par utilisateur (multi-device support)
private static final Map<UUID, Set<WebSocketConnection>> userConnections = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(WebSocketConnection connection, String userId) {
try {
UUID userUUID = UUID.fromString(userId);
// Ajouter la connexion à l'ensemble des connexions de l'utilisateur
userConnections.computeIfAbsent(userUUID, k -> ConcurrentHashMap.newKeySet())
.add(connection);
Log.info("[WS-NEXT] Connexion ouverte pour l'utilisateur: " + userId +
" (Total: " + userConnections.get(userUUID).size() + ")");
// Envoyer confirmation
connection.sendText("{\"type\":\"connected\",\"timestamp\":" +
System.currentTimeMillis() + "}");
} catch (IllegalArgumentException e) {
Log.error("[WS-NEXT] UUID invalide: " + userId, e);
connection.close();
}
}
@OnClose
public void onClose(String userId) {
try {
UUID userUUID = UUID.fromString(userId);
Set<WebSocketConnection> connections = userConnections.get(userUUID);
if (connections != null) {
connections.removeIf(conn -> !conn.isOpen());
if (connections.isEmpty()) {
userConnections.remove(userUUID);
Log.info("[WS-NEXT] Toutes les connexions fermées pour: " + userId);
} else {
Log.info("[WS-NEXT] Connexion fermée pour: " + userId +
" (Restantes: " + connections.size() + ")");
}
}
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur lors de la fermeture", e);
}
}
@OnTextMessage
public void onMessage(String message, String userId) {
try {
Log.debug("[WS-NEXT] Message reçu de " + userId + ": " + message);
// Parser le message JSON
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> messageData = mapper.readValue(message, Map.class);
String type = (String) messageData.get("type");
switch (type) {
case "ping":
handlePing(userId);
break;
case "ack":
handleAck(messageData, userId);
break;
default:
Log.warn("[WS-NEXT] Type de message inconnu: " + type);
}
} catch (Exception e) {
Log.error("[WS-NEXT] Erreur traitement message", e);
}
}
private void handlePing(String userId) {
UUID userUUID = UUID.fromString(userId);
Set<WebSocketConnection> connections = userConnections.get(userUUID);
if (connections != null) {
String pong = "{\"type\":\"pong\",\"timestamp\":" +
System.currentTimeMillis() + "}";
connections.forEach(conn -> {
if (conn.isOpen()) {
conn.sendText(pong);
}
});
}
}
private void handleAck(Map<String, Object> messageData, String userId) {
String notificationId = (String) messageData.get("notificationId");
Log.debug("[WS-NEXT] ACK reçu pour notification " + notificationId +
" de " + userId);
}
/**
* Envoie une notification à un utilisateur spécifique.
* Appelé par le bridge Kafka → WebSocket.
*/
public static void sendToUser(UUID userId, String message) {
Set<WebSocketConnection> connections = userConnections.get(userId);
if (connections == null || connections.isEmpty()) {
Log.debug("[WS-NEXT] Utilisateur " + userId + " non connecté");
return;
}
int success = 0;
int failed = 0;
for (WebSocketConnection conn : connections) {
if (conn.isOpen()) {
try {
conn.sendText(message);
success++;
} catch (Exception e) {
failed++;
Log.error("[WS-NEXT] Erreur envoi à " + userId, e);
}
} else {
failed++;
}
}
Log.info("[WS-NEXT] Notification envoyée à " + userId +
" (Succès: " + success + ", Échec: " + failed + ")");
}
}
```
---
## 🌉 Étape 5 : Bridge Kafka → WebSocket
### NotificationKafkaBridge.java
```java
package com.lions.dev.websocket;
import com.lions.dev.dto.events.NotificationEvent;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import java.util.UUID;
/**
* Bridge qui consomme depuis Kafka et envoie via WebSocket.
*
* Architecture:
* Kafka Topic (notifications) → Bridge → WebSocket (NotificationWebSocketNext)
*/
@ApplicationScoped
public class NotificationKafkaBridge {
/**
* Consomme les événements depuis Kafka et les route vers WebSocket.
*/
@Incoming("kafka-notifications")
public void processNotification(Message<NotificationEvent> message) {
try {
NotificationEvent event = message.getPayload();
Log.debug("[KAFKA-BRIDGE] Événement reçu: " + event.getType() +
" pour utilisateur: " + event.getUserId());
UUID userId = UUID.fromString(event.getUserId());
// Construire le message JSON pour WebSocket
String wsMessage = buildWebSocketMessage(event);
// Envoyer via WebSocket
NotificationWebSocketNext.sendToUser(userId, wsMessage);
// Acknowledger le message Kafka
message.ack();
} catch (Exception e) {
Log.error("[KAFKA-BRIDGE] Erreur traitement événement", e);
message.nack(e);
}
}
private String buildWebSocketMessage(NotificationEvent event) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
java.util.Map<String, Object> wsMessage = java.util.Map.of(
"type", event.getType(),
"data", event.getData(),
"timestamp", event.getTimestamp()
);
return mapper.writeValueAsString(wsMessage);
} catch (Exception e) {
Log.error("[KAFKA-BRIDGE] Erreur construction message", e);
return "{\"type\":\"error\",\"message\":\"Erreur de traitement\"}";
}
}
}
```
---
## 📤 Étape 6 : Services Publient dans Kafka
### FriendshipService (Modifié)
```java
package com.lions.dev.service;
import com.lions.dev.dto.events.NotificationEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.inject.Inject;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class FriendshipService {
@Inject
@Channel("notifications")
Emitter<NotificationEvent> notificationEmitter;
// ... autres dépendances ...
/**
* Envoie une demande d'amitié (publie dans Kafka).
*/
@Transactional
public FriendshipCreateOneResponseDTO sendFriendRequest(
FriendshipCreateOneRequestDTO request) {
// ... logique métier existante ...
// ✅ NOUVEAU: Publier dans Kafka au lieu d'appeler directement WebSocket
try {
NotificationEvent event = new NotificationEvent(
request.getFriendId().toString(), // userId destinataire
"friend_request",
java.util.Map.of(
"fromUserId", request.getUserId().toString(),
"fromFirstName", user.getFirstName(),
"fromLastName", user.getLastName(),
"requestId", response.getFriendshipId().toString()
)
);
notificationEmitter.send(event);
logger.info("[LOG] Événement friend_request publié dans Kafka pour: " +
request.getFriendId());
} catch (Exception e) {
logger.error("[ERROR] Erreur publication Kafka", e);
// Ne pas bloquer la demande d'amitié si Kafka échoue
}
return response;
}
/**
* Accepte une demande d'amitié (publie dans Kafka).
*/
@Transactional
public FriendshipCreateOneResponseDTO acceptFriendRequest(UUID friendshipId) {
// ... logique métier existante ...
// ✅ NOUVEAU: Publier dans Kafka
try {
NotificationEvent event = new NotificationEvent(
originalRequest.getUserId().toString(), // userId émetteur
"friend_request_accepted",
java.util.Map.of(
"friendId", friend.getId().toString(),
"friendFirstName", friend.getFirstName(),
"friendLastName", friend.getLastName(),
"friendshipId", response.getFriendshipId().toString()
)
);
notificationEmitter.send(event);
logger.info("[LOG] Événement friend_request_accepted publié dans Kafka");
} catch (Exception e) {
logger.error("[ERROR] Erreur publication Kafka", e);
}
return response;
}
}
```
### MessageService (Modifié)
```java
package com.lions.dev.service;
import com.lions.dev.dto.events.ChatMessageEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.inject.Inject;
@ApplicationScoped
public class MessageService {
@Inject
@Channel("chat-messages")
Emitter<ChatMessageEvent> chatMessageEmitter;
/**
* Envoie un message (publie dans Kafka).
*/
@Transactional
public MessageResponseDTO sendMessage(SendMessageRequestDTO request) {
// ... logique métier existante ...
// ✅ NOUVEAU: Publier dans Kafka
try {
ChatMessageEvent event = new ChatMessageEvent();
event.setConversationId(conversation.getId().toString());
event.setSenderId(senderId.toString());
event.setRecipientId(recipientId.toString());
event.setContent(request.getContent());
event.setMessageId(message.getId().toString());
event.setTimestamp(System.currentTimeMillis());
// Utiliser conversationId comme clé Kafka pour garantir l'ordre
chatMessageEmitter.send(org.eclipse.microprofile.reactive.messaging.Message.of(
event,
() -> CompletableFuture.completedFuture(null), // ack
throwable -> {
logger.error("[ERROR] Erreur envoi Kafka", throwable);
return CompletableFuture.completedFuture(null); // nack
}
).addMetadata(org.eclipse.microprofile.reactive.messaging.OutgoingMessageMetadata.builder()
.withKey(conversation.getId().toString())
.build()));
logger.info("[LOG] Message publié dans Kafka: " + message.getId());
} catch (Exception e) {
logger.error("[ERROR] Erreur publication Kafka", e);
// Ne pas bloquer l'envoi du message si Kafka échoue
}
return response;
}
}
```
### SocialPostService (Modifié pour les Réactions)
```java
package com.lions.dev.service;
import com.lions.dev.dto.events.ReactionEvent;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.inject.Inject;
@ApplicationScoped
public class SocialPostService {
@Inject
@Channel("reactions")
Emitter<ReactionEvent> reactionEmitter;
/**
* Like un post (publie dans Kafka).
*/
@Transactional
public SocialPost likePost(UUID postId, UUID userId) {
// ... logique métier existante ...
// ✅ NOUVEAU: Publier dans Kafka pour notifier en temps réel
try {
ReactionEvent event = new ReactionEvent();
event.setPostId(postId.toString());
event.setUserId(userId.toString());
event.setReactionType("like");
event.setData(java.util.Map.of(
"postId", postId.toString(),
"userId", userId.toString(),
"likesCount", post.getLikesCount()
));
event.setTimestamp(System.currentTimeMillis());
reactionEmitter.send(event);
logger.info("[LOG] Réaction like publiée dans Kafka pour post: " + postId);
} catch (Exception e) {
logger.error("[ERROR] Erreur publication Kafka", e);
}
return post;
}
}
```
---
## 🎨 Frontend : Amélioration du Service WebSocket
### realtime_notification_service_v2.dart
```dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
class RealtimeNotificationServiceV2 extends ChangeNotifier {
RealtimeNotificationServiceV2(this.userId, this.authToken);
final String userId;
final String authToken;
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _heartbeatTimer;
Timer? _reconnectTimer;
bool _isConnected = false;
bool get isConnected => _isConnected;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _heartbeatInterval = Duration(seconds: 30);
static const Duration _reconnectDelay = Duration(seconds: 5);
// Streams pour différents types d'événements
final _friendRequestController = StreamController<Map<String, dynamic>>.broadcast();
final _systemNotificationController = StreamController<Map<String, dynamic>>.broadcast();
final _reactionController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get friendRequestStream => _friendRequestController.stream;
Stream<Map<String, dynamic>> get systemNotificationStream => _systemNotificationController.stream;
Stream<Map<String, dynamic>> get reactionStream => _reactionController.stream;
String get _wsUrl {
final baseUrl = 'wss://api.afterwork.lions.dev'; // Production
return '$baseUrl/notifications/$userId';
}
Future<void> connect() async {
if (_isConnected) return;
try {
_channel = WebSocketChannel.connect(
Uri.parse(_wsUrl),
protocols: ['notifications-v2'],
headers: {
'Authorization': 'Bearer $authToken',
},
);
// Heartbeat pour maintenir la connexion
_heartbeatTimer = Timer.periodic(_heartbeatInterval, (_) {
_channel?.sink.add(jsonEncode({'type': 'ping'}));
});
// Écouter les messages
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDisconnection,
cancelOnError: false,
);
_isConnected = true;
notifyListeners();
} catch (e) {
_isConnected = false;
notifyListeners();
_scheduleReconnect();
}
}
void _handleMessage(dynamic message) {
try {
final data = jsonDecode(message as String) as Map<String, dynamic>;
final type = data['type'] as String;
switch (type) {
case 'connected':
_reconnectAttempts = 0; // Reset sur reconnexion réussie
break;
case 'pong':
// Heartbeat réponse
break;
case 'friend_request':
case 'friend_request_accepted':
_friendRequestController.add(data);
break;
case 'event_reminder':
case 'system_notification':
_systemNotificationController.add(data);
break;
case 'reaction':
_reactionController.add(data);
break;
default:
// Type inconnu, ignorer ou logger
break;
}
} catch (e) {
// Erreur de parsing, ignorer
}
}
void _handleError(dynamic error) {
_isConnected = false;
notifyListeners();
_scheduleReconnect();
}
void _handleDisconnection() {
_isConnected = false;
notifyListeners();
_scheduleReconnect();
}
void _scheduleReconnect() {
if (_reconnectAttempts >= _maxReconnectAttempts) {
// Arrêter les tentatives après max
return;
}
_reconnectTimer?.cancel();
_reconnectTimer = Timer(_reconnectDelay * (_reconnectAttempts + 1), () {
_reconnectAttempts++;
connect();
});
}
Future<void> disconnect() async {
_heartbeatTimer?.cancel();
_reconnectTimer?.cancel();
await _subscription?.cancel();
await _channel?.sink.close(status.normalClosure);
_isConnected = false;
notifyListeners();
}
@override
void dispose() {
disconnect();
_friendRequestController.close();
_systemNotificationController.close();
_reactionController.close();
super.dispose();
}
}
```
---
## 🧪 Tests
### Test du Bridge Kafka → WebSocket
```java
package com.lions.dev.websocket;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import org.junit.jupiter.api.Test;
@QuarkusTest
public class NotificationKafkaBridgeTest {
@Inject
@Channel("notifications")
Emitter<NotificationEvent> notificationEmitter;
@Test
public void testNotificationFlow() {
// Publier un événement dans Kafka
NotificationEvent event = new NotificationEvent(
"user-123",
"friend_request",
Map.of("fromUserId", "user-456")
);
notificationEmitter.send(event);
// Vérifier que le message arrive bien via WebSocket
// (nécessite un client WebSocket de test)
}
}
```
---
## 📊 Monitoring
### Métriques Kafka à Surveiller
1. **Lag Consumer** : Délai entre production et consommation
2. **Throughput** : Messages/seconde
3. **Error Rate** : Taux d'erreur
4. **Connection Count** : Nombre de connexions WebSocket actives
### Endpoint de Santé
```java
@Path("/health/realtime")
public class RealtimeHealthResource {
@GET
public Response health() {
return Response.ok(Map.of(
"websocket_connections", NotificationWebSocketNext.getConnectionCount(),
"kafka_consumers", getKafkaConsumerCount(),
"status", "healthy"
)).build();
}
}
```
---
## 🚀 Déploiement
### Docker Compose (Kafka Local)
```yaml
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
```
### Production (Kubernetes)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: afterwork-backend
spec:
replicas: 3 # ✅ Scalabilité horizontale
template:
spec:
containers:
- name: quarkus
env:
- name: KAFKA_BOOTSTRAP_SERVERS
value: "kafka-service:9092"
```
---
## ✅ Checklist d'Implémentation
### Phase 1 : Setup
- [ ] Ajouter dépendances dans `pom.xml`
- [ ] Configurer `application.properties`
- [ ] Tester Kafka avec Quarkus Dev Services
- [ ] Créer les DTOs d'événements
### Phase 2 : Migration WebSocket
- [ ] Créer `NotificationWebSocketNext`
- [ ] Créer `ChatWebSocketNext`
- [ ] Tester avec le frontend existant
- [ ] Comparer performances (avant/après)
### Phase 3 : Intégration Kafka
- [ ] Créer `NotificationKafkaBridge`
- [ ] Créer `ChatKafkaBridge`
- [ ] Modifier `FriendshipService` pour publier dans Kafka
- [ ] Modifier `MessageService` pour publier dans Kafka
- [ ] Modifier `SocialPostService` pour les réactions
### Phase 4 : Frontend
- [ ] Améliorer `RealtimeNotificationService` avec heartbeat
- [ ] Améliorer `ChatWebSocketService` avec reconnect
- [ ] Tester la reconnexion automatique
- [ ] Tester multi-device
### Phase 5 : Tests & Monitoring
- [ ] Tests unitaires des bridges
- [ ] Tests d'intégration end-to-end
- [ ] Configurer monitoring Kafka
- [ ] Configurer alertes
---
## 📚 Ressources Complémentaires
- [Quarkus WebSockets Next Tutorial](https://quarkus.io/guides/websockets-next-tutorial)
- [Quarkus Kafka Guide](https://quarkus.io/guides/kafka)
- [Reactive Messaging HTTP Extension](https://docs.quarkiverse.io/quarkus-reactive-messaging-http/dev/reactive-messaging-websocket.html)
- [Kafka Best Practices](https://kafka.apache.org/documentation/#bestPractices)

View File

@@ -1,33 +0,0 @@
# Sécurité AfterWork Backend
## Authentification et autorisation
- **Super Admin** : Les opérations réservées au super administrateur (stats admin, modification de rôle utilisateur, impersonation) exigent le header `X-Super-Admin-Key` dont la valeur doit correspondre à la propriété `afterwork.super-admin.api-key` (ou `SUPER_ADMIN_API_KEY` en production). À configurer uniquement côté serveur, jamais exposée au client.
- **Utilisateurs / rôles** : À ce jour, lAPI ne repose pas sur JWT/OAuth pour les endpoints métier. En production, il est recommandé dajouter un filtre ou une ressource qui dérive lidentité (userId) du token (JWT/session) et de **ne pas faire confiance au `userId` passé dans lURL** (ex. `GET /notifications/user/{userId}`). L`userId` utilisé doit être celui de lutilisateur authentifié.
## Endpoints sensibles
- **Notifications** (`/notifications/user/{userId}`) : En létat, tout appelant peut demander les notifications dun autre utilisateur en changeant `userId`. En production, remplacer `userId` par lidentifiant issu du contexte dauthentification (JWT/subject).
- **Admin** : `AdminStatsResource` et les endpoints de modification de rôle dans `UsersResource` sont protégés par `X-Super-Admin-Key`.
## Webhook Wave
- **Signature** : Si la propriété `wave.webhook.secret` (ou `WAVE_WEBHOOK_SECRET`) est renseignée, le endpoint `/webhooks/wave` vérifie le header `X-Wave-Signature` (HMAC-SHA256 du body avec ce secret). Sans secret configuré, la vérification est désactivée (acceptable uniquement en dev/test).
- **Production** : Configurer systématiquement `WAVE_WEBHOOK_SECRET` avec le secret fourni par Wave pour éviter les appels forgés.
## Secrets et configuration
- **Base de données** : Utiliser les variables denvironnement (ex. `DB_USERNAME`, `DB_PASSWORD`) ou le profil Quarkus ; ne pas committer de mots de passe en clair.
- **Wave** : `WAVE_API_KEY` et `WAVE_WEBHOOK_SECRET` via variables denvironnement.
- **Email (SMTP)** : `MAILER_USERNAME`, `MAILER_PASSWORD` (et optionnellement `MAILER_FROM`, `MAILER_HOST`, etc.) via variables denvironnement.
- **Super Admin** : `SUPER_ADMIN_EMAIL`, `SUPER_ADMIN_PASSWORD`, `SUPER_ADMIN_API_KEY` pour la production.
## Validation des entrées
- Les DTOs utilisent Bean Validation (`@Valid`, `@NotNull`, `@Size`, `@Email`, `@Pattern`) sur les endpoints principaux (création utilisateur, authentification, établissements, abonnements, etc.). Conserver et étendre ces contraintes sur tout nouvel endpoint.
## Bonnes pratiques
- Répondre par des codes HTTP adaptés (401 si non autorisé, 403 si interdit, 404 si ressource absente).
- Ne pas logger de secrets (tokens, mots de passe, clés API).
- En production, utiliser HTTPS et limiter lexposition des headers sensibles (CORS, sécurisation des headers).

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
##
## AfterWork Server - Development Dockerfile
## Image légère avec JRE Alpine (JAR pré-buildé requis)
##
FROM eclipse-temurin:17-jre-alpine
# Variables d'environnement
ENV LANG='en_US.UTF-8' \
QUARKUS_PROFILE=dev \
JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC"
# Installation des dépendances système
RUN apk add --no-cache curl tzdata && \
cp /usr/share/zoneinfo/Africa/Douala /etc/localtime && \
echo "Africa/Douala" > /etc/timezone
# Création du user non-root
RUN addgroup -g 185 -S appuser && \
adduser -u 185 -S appuser -G appuser
# Création des répertoires
RUN mkdir -p /app /tmp/uploads && \
chown -R appuser:appuser /app /tmp/uploads
WORKDIR /app
# Copie du JAR (context = racine du projet, build après mvn package)
COPY --chown=appuser:appuser target/*-runner.jar /app/app.jar
# Exposition du port
EXPOSE 8080
# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
# User non-root
USER appuser
# Lancement
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

View File

@@ -1,61 +0,0 @@
##
## AfterWork Server - Production Dockerfile
## Build stage avec Maven + Runtime optimisé avec UBI8 OpenJDK 17
##
# ======================================
# STAGE 1: Build de l'application
# ======================================
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 AS builder
USER root
# Installation de Maven
RUN microdnf install -y maven && microdnf clean all
# Copie des fichiers du projet (context = racine du projet)
WORKDIR /build
COPY pom.xml .
COPY src ./src
# Build de l'application (skip tests pour accélérer)
RUN mvn clean package -DskipTests -Dquarkus.package.type=uber-jar
# ======================================
# STAGE 2: Image de runtime
# ======================================
FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18
# Variables d'environnement par défaut
ENV LANG='en_US.UTF-8' \
LANGUAGE='en_US:en' \
TZ='Africa/Douala' \
QUARKUS_PROFILE=prod \
DB_HOST=postgresql \
DB_PORT=5432 \
DB_NAME=afterwork_db \
DB_USERNAME=afterwork \
DB_PASSWORD=changeme \
JAVA_OPTS_APPEND="-XX:+UseG1GC \
-XX:+StringDeduplication \
-XX:+OptimizeStringConcat \
-XX:MaxRAMPercentage=75.0 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof \
-Djava.net.preferIPv4Stack=true"
# Configuration du port
EXPOSE 8080
# Copie de l'uber-jar depuis le builder
COPY --from=builder --chown=185:185 /build/target/*-runner.jar /deployments/app.jar
# User non-root pour la sécurité
USER 185
# Healthcheck sur l'endpoint Quarkus
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/q/health/ready || exit 1
# Lancement de l'application
ENTRYPOINT ["java", "-jar", "/deployments/app.jar"]

View File

@@ -1,54 +0,0 @@
# Docker AfterWork
Fichiers Docker pour le build et lexécution de lAPI 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 lhô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 dexécution sur lhô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)
Lapplication se connecte à PostgreSQL sur lhôte (`host.docker.internal:5432`). Sans identifiants, lerreur **« 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 lutilisateur 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/`.

View File

@@ -1,26 +0,0 @@
# 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

View File

@@ -1,2 +0,0 @@
# ConfigMap déplacé dans afterwork-secrets.yaml pour cohérence
# Voir afterwork-secrets.yaml pour la configuration complète

View File

@@ -1,156 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mic-after-work-server-impl-quarkus-main
namespace: applications
labels:
app: mic-after-work-server-impl-quarkus-main
version: "1.0.0"
environment: production
component: application
project: lions-infrastructure-2025
annotations:
description: "AfterWork API - Application sociale déployée via lionsctl"
lionsctl.lions.dev/deployed-by: "lionsctl"
spec:
replicas: 1
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: mic-after-work-server-impl-quarkus-main
template:
metadata:
labels:
app: mic-after-work-server-impl-quarkus-main
version: "1.0.0"
component: application
project: lions-infrastructure-2025
annotations:
# Prometheus scraping - Lions Prometheus auto-découvre via ces annotations
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/afterwork/q/metrics"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile:
type: RuntimeDefault
terminationGracePeriodSeconds: 30
containers:
- name: mic-after-work-server-impl-quarkus-main
image: registry.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
protocol: TCP
# Variables d'environnement depuis ConfigMap et Secrets
envFrom:
- configMapRef:
name: afterwork-config
- secretRef:
name: afterwork-secrets
env:
# Override explicites pour Quarkus
- name: QUARKUS_DATASOURCE_DB_KIND
value: "postgresql"
- name: QUARKUS_DATASOURCE_USERNAME
valueFrom:
configMapKeyRef:
name: afterwork-config
key: DB_USERNAME
- name: QUARKUS_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: afterwork-secrets
key: DB_PASSWORD
- name: QUARKUS_DATASOURCE_JDBC_URL
value: "jdbc:postgresql://$(DB_HOST):$(DB_PORT)/$(DB_NAME)"
# Kafka - Lions Kafka cluster
- name: KAFKA_BOOTSTRAP_SERVERS
valueFrom:
configMapKeyRef:
name: afterwork-config
key: KAFKA_BOOTSTRAP_SERVERS
# JWT
- name: SMALLRYE_JWT_SIGN_KEY
valueFrom:
secretKeyRef:
name: afterwork-secrets
key: JWT_SECRET
- name: MP_JWT_VERIFY_ISSUER
valueFrom:
configMapKeyRef:
name: afterwork-config
key: JWT_ISSUER
# Java options
- name: JAVA_OPTS
value: "-Xms256m -Xmx512m -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1000m"
# Health checks HTTP (utilisent les endpoints SmallRye Health)
livenessProbe:
httpGet:
path: /afterwork/q/health/live
port: 8080
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 3
readinessProbe:
httpGet:
path: /afterwork/q/health/ready
port: 8080
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
# Startup probe pour éviter les kills pendant le démarrage
startupProbe:
httpGet:
path: /afterwork/q/health/started
port: 8080
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp-volume
mountPath: /tmp
- name: logs-volume
mountPath: /app/logs
volumes:
- name: tmp-volume
emptyDir: {}
- name: logs-volume
emptyDir: {}
imagePullSecrets:
- name: lionsregistry-secret
restartPolicy: Always

View File

@@ -1,68 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mic-after-work-server-impl-quarkus-main-ingress
namespace: applications
labels:
app: mic-after-work-server-impl-quarkus-main
annotations:
# SSL/TLS
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
# Metadata
description: "Ingress for afterwork-api application on api.lions.dev/afterwork"
kubernetes.io/ingress.class: nginx
lionsctl.lions.dev/deployed-by: lionsctl
lionsctl.lions.dev/domain: api.lions.dev
lionsctl.lions.dev/path: /afterwork
# Proxy settings
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-buffering: "on"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
# WebSocket support
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/websocket-services: "mic-after-work-server-impl-quarkus-main-service"
# Security headers and CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS, PATCH"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
nginx.ingress.kubernetes.io/cors-expose-headers: "Content-Length,Content-Range,Content-Disposition"
nginx.ingress.kubernetes.io/cors-max-age: "86400"
# Compression
nginx.ingress.kubernetes.io/enable-compression: "true"
nginx.ingress.kubernetes.io/compression-types: "text/plain,text/css,application/json,application/javascript,text/xml,application/xml,application/xml+rss,text/javascript"
# Rate limiting
nginx.ingress.kubernetes.io/rate-limit: "1000"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
# PAS de rewrite-target : le backend sert sous quarkus.http.root-path=/afterwork,
# l'Ingress doit transmettre le chemin complet (/afterwork/...) au service.
spec:
ingressClassName: nginx
tls:
- hosts:
- api.lions.dev
secretName: api-lions-dev-tls
rules:
- host: api.lions.dev
http:
paths:
- path: /afterwork
pathType: Prefix
backend:
service:
name: mic-after-work-server-impl-quarkus-main-service
port:
number: 80

View File

@@ -1,408 +0,0 @@
# ==============================================================================
# AfterWork API - Configuration Monitoring pour Lions Infrastructure
# ==============================================================================
# Cette configuration intègre l'application avec:
# - Prometheus (https://prometheus.lions.dev) - scraping auto via annotations
# - Grafana (https://grafana.lions.dev) - dashboard dédié
# ==============================================================================
---
# ==============================================================================
# ServiceMonitor pour Prometheus Operator (si installé)
# ==============================================================================
# Note: L'infrastructure Lions utilise le scraping via annotations pod, mais
# ce ServiceMonitor peut être utilisé si Prometheus Operator est déployé.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: afterwork-api-monitor
namespace: monitoring
labels:
app: mic-after-work-server-impl-quarkus-main
release: prometheus
project: lions-infrastructure-2025
spec:
selector:
matchLabels:
app: mic-after-work-server-impl-quarkus-main
namespaceSelector:
matchNames:
- applications
endpoints:
- port: http-direct
path: /afterwork/q/metrics
interval: 30s
scrapeTimeout: 10s
scheme: http
---
# ==============================================================================
# PrometheusRule - Alertes pour AfterWork API
# ==============================================================================
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: afterwork-api-alerts
namespace: monitoring
labels:
app: mic-after-work-server-impl-quarkus-main
release: prometheus
project: lions-infrastructure-2025
spec:
groups:
- name: afterwork-api.rules
rules:
# Alerte si l'application est down
- alert: AfterWorkAPIDown
expr: up{job=~".*afterwork.*"} == 0
for: 2m
labels:
severity: critical
application: afterwork-api
annotations:
summary: "AfterWork API is down"
description: "L'API AfterWork n'est pas accessible depuis plus de 2 minutes"
# Alerte si le taux d'erreur HTTP 5xx est élevé
- alert: AfterWorkHighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{
kubernetes_namespace="applications",
app="mic-after-work-server-impl-quarkus-main",
status=~"5.."
}[5m])) /
sum(rate(http_server_requests_seconds_count{
kubernetes_namespace="applications",
app="mic-after-work-server-impl-quarkus-main"
}[5m])) > 0.05
for: 5m
labels:
severity: warning
application: afterwork-api
annotations:
summary: "High error rate on AfterWork API"
description: "Le taux d'erreur 5xx est supérieur à 5% depuis 5 minutes"
# Alerte si la latence p95 est élevée
- alert: AfterWorkHighLatency
expr: |
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{
kubernetes_namespace="applications",
app="mic-after-work-server-impl-quarkus-main"
}[5m])) by (le)) > 2
for: 5m
labels:
severity: warning
application: afterwork-api
annotations:
summary: "High latency on AfterWork API"
description: "La latence p95 dépasse 2 secondes depuis 5 minutes"
# Alerte si la mémoire est proche de la limite
- alert: AfterWorkHighMemoryUsage
expr: |
sum(container_memory_working_set_bytes{
namespace="applications",
pod=~"mic-after-work-server-impl-quarkus-main.*"
}) /
sum(container_spec_memory_limit_bytes{
namespace="applications",
pod=~"mic-after-work-server-impl-quarkus-main.*"
}) > 0.85
for: 5m
labels:
severity: warning
application: afterwork-api
annotations:
summary: "High memory usage on AfterWork API"
description: "L'utilisation mémoire dépasse 85% de la limite"
# Alerte si le pod redémarre fréquemment
- alert: AfterWorkPodRestarts
expr: |
increase(kube_pod_container_status_restarts_total{
namespace="applications",
pod=~"mic-after-work-server-impl-quarkus-main.*"
}[1h]) > 3
for: 5m
labels:
severity: warning
application: afterwork-api
annotations:
summary: "AfterWork API pod restarting frequently"
description: "Le pod a redémarré plus de 3 fois dans la dernière heure"
---
# ==============================================================================
# Grafana Dashboard ConfigMap (pour import automatique)
# ==============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: afterwork-grafana-dashboard
namespace: monitoring
labels:
grafana_dashboard: "1"
app: mic-after-work-server-impl-quarkus-main
project: lions-infrastructure-2025
data:
afterwork-api-dashboard.json: |
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 100},
{"color": "red", "value": 500}
]
},
"unit": "reqps"
}
},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"id": 1,
"options": {},
"targets": [
{
"expr": "sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m]))",
"legendFormat": "Requests/s",
"refId": "A"
}
],
"title": "Request Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "ms"
}
},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"id": 2,
"options": {},
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) by (le)) * 1000",
"legendFormat": "p95 Latency",
"refId": "A"
},
{
"expr": "histogram_quantile(0.50, sum(rate(http_server_requests_seconds_bucket{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) by (le)) * 1000",
"legendFormat": "p50 Latency",
"refId": "B"
}
],
"title": "Response Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent"
}
},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
"id": 3,
"options": {},
"targets": [
{
"expr": "sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) * 100",
"legendFormat": "Error Rate %",
"refId": "A"
}
],
"title": "Error Rate",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "bytes"
}
},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
"id": 4,
"options": {},
"targets": [
{
"expr": "sum(container_memory_working_set_bytes{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"})",
"legendFormat": "Memory Used",
"refId": "A"
},
{
"expr": "sum(container_spec_memory_limit_bytes{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"})",
"legendFormat": "Memory Limit",
"refId": "B"
}
],
"title": "Memory Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short"
}
},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
"id": 5,
"options": {},
"targets": [
{
"expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"}[5m])) * 1000",
"legendFormat": "CPU Usage (millicores)",
"refId": "A"
}
],
"title": "CPU Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "green", "value": 1}
]
}
}
},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 16},
"id": 6,
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"targets": [
{
"expr": "up{job=~\".*afterwork.*\"}",
"legendFormat": "Status",
"refId": "A"
}
],
"title": "API Status",
"type": "gauge"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 1},
{"color": "red", "value": 3}
]
}
}
},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 16},
"id": 7,
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
}
},
"targets": [
{
"expr": "increase(kube_pod_container_status_restarts_total{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"}[1h])",
"legendFormat": "Restarts (1h)",
"refId": "A"
}
],
"title": "Pod Restarts (1h)",
"type": "stat"
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["lions", "afterwork", "quarkus", "api"],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "AfterWork API Dashboard",
"uid": "afterwork-api",
"version": 1,
"weekStart": ""
}

View File

@@ -1,180 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: afterwork-secrets
namespace: applications
labels:
app: afterwork-api
component: secrets
environment: production
project: lions-infrastructure-2025
type: Opaque
stringData:
# ==============================================================================
# BASE DE DONNÉES PostgreSQL
# ==============================================================================
# Utilise le PostgreSQL de l'infrastructure Lions
# postgresql-service.postgresql.svc.cluster.local:5432
DB_PASSWORD: "AfterWork2025!"
# ==============================================================================
# JWT / SÉCURITÉ
# ==============================================================================
# Clé secrète JWT (minimum 32 caractères, aléatoire)
# Générer avec: openssl rand -base64 32
JWT_SECRET: "AfterWorkJWTSecret2025LionsInfrastructureKey"
# ==============================================================================
# COMPTE ADMINISTRATEUR INITIAL
# ==============================================================================
ADMIN_EMAIL: "admin@afterwork.ci"
ADMIN_PASSWORD: "AdminAfterWork2025!"
# ==============================================================================
# SERVICE EMAIL (SMTP)
# ==============================================================================
# Configuration Gmail ou autre SMTP
MAILER_USERNAME: "noreply@afterwork.ci"
MAILER_PASSWORD: "CHANGEZ_MOI_SMTP_PASSWORD"
# ==============================================================================
# WAVE PAYMENT (Intégration paiement)
# ==============================================================================
WAVE_API_KEY: "CHANGEZ_MOI_WAVE_API_KEY"
WAVE_SECRET: "CHANGEZ_MOI_WAVE_SECRET"
---
# ==============================================================================
# CONFIGMAP POUR CONFIGURATION NON-SENSIBLE
# ==============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: afterwork-config
namespace: applications
labels:
app: afterwork-api
component: configuration
environment: production
project: lions-infrastructure-2025
data:
# ==============================================================================
# BASE DE DONNÉES - Lions PostgreSQL
# ==============================================================================
DB_HOST: "postgresql-service.postgresql.svc.cluster.local"
DB_PORT: "5432"
DB_NAME: "mic-after-work-server-impl-quarkus-main"
DB_USERNAME: "lionsuser"
# ==============================================================================
# QUARKUS
# ==============================================================================
QUARKUS_PROFILE: "prod"
QUARKUS_LOG_LEVEL: "INFO"
QUARKUS_LOG_CONSOLE_JSON: "true"
# ==============================================================================
# JWT
# ==============================================================================
JWT_LIFESPAN: "86400"
JWT_ISSUER: "afterwork-api"
# ==============================================================================
# KAFKA - Lions Infrastructure
# ==============================================================================
# Utilise le Kafka déployé dans le namespace kafka
KAFKA_BOOTSTRAP_SERVERS: "kafka-service.kafka.svc.cluster.local:9092"
# ==============================================================================
# EMAIL (SMTP)
# ==============================================================================
MAILER_HOST: "smtp.gmail.com"
MAILER_PORT: "587"
MAILER_FROM: "AfterWork <noreply@afterwork.ci>"
MAILER_START_TLS: "REQUIRED"
# En production, mettre false. true = mock (pas d'envoi réel)
MAILER_MOCK: "true"
# ==============================================================================
# RATE LIMITING
# ==============================================================================
AFTERWORK_RATELIMIT_MAX_REQUESTS: "10"
AFTERWORK_RATELIMIT_WINDOW_SECONDS: "60"
# ==============================================================================
# WAVE PAYMENT
# ==============================================================================
WAVE_BASE_URL: "https://api.wave.com"
WAVE_CURRENCY: "XOF"
WAVE_CALLBACK_URL: "https://api.lions.dev/afterwork/webhooks/wave"
# ==============================================================================
# OBSERVABILITY - Lions Prometheus/Grafana
# ==============================================================================
# Prometheus scrape via annotations sur le pod
# Grafana disponible sur https://grafana.lions.dev
# ==============================================================================
# KEYCLOAK / SSO (optionnel)
# ==============================================================================
# OIDC_AUTH_SERVER_URL: "https://security.lions.dev/realms/lions"
# OIDC_CLIENT_ID: "afterwork-api"
---
# ==============================================================================
# EXTERNAL SECRET - Intégration Vault (ACTIF)
# ==============================================================================
# Vault est déverrouillé sur https://vault.lions.dev
# Les secrets sont synchronisés depuis Vault vers Kubernetes automatiquement
#
# PRÉREQUIS: Créer les secrets dans Vault avec:
# vault kv put lions/afterwork \
# db_password="AfterWork2025!" \
# jwt_secret="AfterWorkJWTSecret2025LionsInfrastructureKey" \
# admin_password="AdminAfterWork2025!" \
# mailer_password="SMTP_PASSWORD" \
# wave_api_key="WAVE_KEY" \
# wave_secret="WAVE_SECRET"
#
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: afterwork-vault-secrets
namespace: applications
labels:
app: afterwork-api
component: external-secrets
project: lions-infrastructure-2025
spec:
refreshInterval: "1h"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: afterwork-secrets-vault
creationPolicy: Owner
data:
- secretKey: DB_PASSWORD
remoteRef:
key: lions/data/afterwork
property: db_password
- secretKey: JWT_SECRET
remoteRef:
key: lions/data/afterwork
property: jwt_secret
- secretKey: ADMIN_PASSWORD
remoteRef:
key: lions/data/afterwork
property: admin_password
- secretKey: MAILER_PASSWORD
remoteRef:
key: lions/data/afterwork
property: mailer_password
- secretKey: WAVE_API_KEY
remoteRef:
key: lions/data/afterwork
property: wave_api_key
- secretKey: WAVE_SECRET
remoteRef:
key: lions/data/afterwork
property: wave_secret

View File

@@ -1,30 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: mic-after-work-server-impl-quarkus-main-service
namespace: applications
labels:
app: mic-after-work-server-impl-quarkus-main
component: application
project: lions-infrastructure-2025
annotations:
description: "Service for AfterWork API"
spec:
type: ClusterIP
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
# Port 80 exposé, route vers 8080 du container
- port: 80
targetPort: 8080
protocol: TCP
name: http
# Port 8080 pour compatibilité directe
- port: 8080
targetPort: 8080
protocol: TCP
name: http-direct
selector:
app: mic-after-work-server-impl-quarkus-main

256
mvnw vendored
View File

@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
@@ -33,84 +33,75 @@
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ]; then
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ]; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ]; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ]; then
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false
darwin=false
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true ;;
Darwin*)
darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"
export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"
export JAVA_HOME
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
fi
;;
;;
esac
if [ -z "$JAVA_HOME" ]; then
if [ -r /etc/gentoo-release ]; then
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin; then
[ -n "$JAVA_HOME" ] \
&& JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] \
&& CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
&& JAVA_HOME="$(
cd "$JAVA_HOME" || (
echo "cannot cd into $JAVA_HOME." >&2
exit 1
)
pwd
)"
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin; then
javaHome="$(dirname "$javaExecutable")"
javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "$javaExecutable")"
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "$javaExecutable")"
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
@@ -118,60 +109,52 @@ if [ -z "$JAVA_HOME" ]; then
fi
fi
if [ -z "$JAVACMD" ]; then
if [ -n "$JAVA_HOME" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(
\unset -f command 2>/dev/null
\command -v java
)"
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ]; then
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ]; then
echo "Warning: JAVA_HOME environment variable is not set." >&2
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]; then
echo "Path not specified to find_maven_basedir" >&2
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ]; do
if [ -d "$wdir"/.mvn ]; then
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(
cd "$wdir/.." || exit 1
pwd
)
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(
cd "$basedir" || exit 1
pwd
)"
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
@@ -182,7 +165,7 @@ concat_lines() {
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' <"$1"
tr -s '\r\n' ' ' < "$1"
fi
}
@@ -194,11 +177,10 @@ log() {
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
export MAVEN_PROJECTBASEDIR
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
@@ -207,66 +189,63 @@ log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in wrapperUrl)
wrapperUrl="$safeValue"
break
;;
esac
done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget >/dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
elif command -v curl >/dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
fi
##########################################################################################
# End of extension
@@ -275,25 +254,22 @@ fi
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in wrapperSha256Sum)
wrapperSha256Sum=$value
break
;;
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum >/dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
@@ -308,12 +284,12 @@ MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] \
&& JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] \
&& CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] \
&& MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will

21
mvnw.cmd vendored
View File

@@ -18,7 +18,7 @@
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@@ -59,22 +59,22 @@ set ERROR_CODE=0
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo. >&2
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo. >&2
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo. >&2
echo.
goto error
@REM ==== END VALIDATION ====
@@ -119,7 +119,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
@@ -133,7 +133,7 @@ if exist %WRAPPER_JAR% (
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
@@ -160,12 +160,11 @@ FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapp
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"

413
pom.xml
View File

@@ -1,244 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.lions</groupId>
<artifactId>mic-after-work-server-impl-quarkus-main</artifactId>
<version>1.0.0-SNAPSHOT</version>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lions.dev</groupId>
<artifactId>mic-after-work-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.16.3</quarkus.platform.version>
<quarkus.package.type>fast-jar</quarkus.package.type>
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.0</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.13.0</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<!-- JWT : émission au login et validation sur les requêtes -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-json</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.groovy</groupId>
<artifactId>quarkus-groovy-junit5</artifactId>
<version>3.16.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<!-- WebSockets Next (remplace quarkus-websockets) -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
<!-- Kafka Reactive Messaging -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-messaging-kafka</artifactId>
</dependency>
<!-- JSON Serialization pour Kafka -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
</dependency>
<!-- Flyway pour les migrations SQL automatiques -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
</dependency>
<!-- Scheduler pour jobs planifiés (nettoyage stories, tokens, rappels événements) -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>at.favre.lib</groupId>
<artifactId>bcrypt</artifactId>
<version>0.10.2</version>
</dependency>
<!-- Email Service pour réinitialisation de mot de passe -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<!-- ============================================== -->
<!-- HEALTH CHECKS & OBSERVABILITY -->
<!-- ============================================== -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
<!-- ============================================== -->
<!-- TEST DEPENDENCIES -->
<!-- ============================================== -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<!-- Ne pas bloquer le build si les tests échouent -->
<testFailureIgnore>true</testFailureIgnore>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Jakarta Bean Validation -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>jakarta.el</groupId>
<artifactId>jakarta.el-api</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
<version>3.13.0</version> <!-- Utilise la même version de Quarkus -->
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.3.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.groovy</groupId>
<artifactId>quarkus-groovy-junit5</artifactId>
<version>3.16.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-oracle</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>

View File

@@ -1,20 +0,0 @@
# Scripts AfterWork
Scripts de déploiement et doutillage.
## 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/`).

View File

@@ -1,309 +0,0 @@
# ====================================================================
# AfterWork Server - Script de Déploiement Production
# ====================================================================
# Ce script automatise le processus de build et déploiement
# de l'API AfterWork sur le VPS via Kubernetes.
# Exécuter depuis la racine du projet ou depuis scripts/
# ====================================================================
param(
[ValidateSet("build", "push", "deploy", "all", "rollback", "status")]
[string]$Action = "all",
[string]$Version = "1.0.0",
[string]$Registry = "registry.lions.dev",
[switch]$SkipTests,
[switch]$Force
)
$ErrorActionPreference = "Stop"
# Racine du projet (parent du dossier scripts)
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
# Couleurs
function Write-Info { param($msg) Write-Host $msg -ForegroundColor Cyan }
function Write-Success { param($msg) Write-Host $msg -ForegroundColor Green }
function Write-Warning { param($msg) Write-Host $msg -ForegroundColor Yellow }
function Write-Error { param($msg) Write-Host $msg -ForegroundColor Red }
# Variables
$AppName = "afterwork-api"
$Namespace = "applications"
$ImageName = "$Registry/${AppName}:$Version"
$ImageLatest = "$Registry/${AppName}:latest"
Write-Info "======================================================================"
Write-Info " AfterWork Server - Déploiement Production"
Write-Info "======================================================================"
Write-Host ""
Write-Info "Configuration:"
Write-Host " - Action: $Action"
Write-Host " - Version: $Version"
Write-Host " - Registry: $Registry"
Write-Host " - Image: $ImageName"
Write-Host " - Namespace: $Namespace"
Write-Host " - Racine projet: $ProjectRoot"
Write-Host ""
# ======================================================================
# Build Maven
# ======================================================================
function Build-Application {
Write-Info "[1/5] Build Maven..."
Push-Location $ProjectRoot
try {
$mavenArgs = "clean", "package", "-Dquarkus.package.type=uber-jar"
if ($SkipTests) {
$mavenArgs += "-DskipTests"
} else {
$mavenArgs += "-DtestFailureIgnore=true"
}
& mvn $mavenArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du build Maven"
exit 1
}
$jar = Get-ChildItem -Path (Join-Path $ProjectRoot "target") -Filter "*-runner.jar" | Select-Object -First 1
if (-not $jar) {
Write-Error "JAR runner non trouvé dans target/"
exit 1
}
Write-Success "Build Maven réussi : $($jar.Name)"
} finally {
Pop-Location
}
}
# ======================================================================
# Build Docker Image
# ======================================================================
function Build-DockerImage {
Write-Info "[2/5] Build Docker Image..."
Push-Location $ProjectRoot
try {
$dockerDir = Join-Path $ProjectRoot "docker"
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"
} finally {
Pop-Location
}
}
# ======================================================================
# Push vers Registry
# ======================================================================
function Push-ToRegistry {
Write-Info "[3/5] Push vers Registry..."
$loginTest = docker login $Registry 2>&1
if ($LASTEXITCODE -ne 0 -and -not $loginTest.ToString().Contains("Succeeded")) {
Write-Warning "Connexion au registry nécessaire..."
docker login $Registry
if ($LASTEXITCODE -ne 0) {
Write-Error "Échec de connexion au registry"
exit 1
}
}
docker push $ImageName
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du push de $ImageName"
exit 1
}
docker push $ImageLatest
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du push de $ImageLatest"
exit 1
}
Write-Success "Images pushées vers $Registry"
}
# ======================================================================
# Déploiement Kubernetes
# ======================================================================
function Deploy-ToKubernetes {
Write-Info "[4/5] Déploiement Kubernetes..."
$kubectlCheck = kubectl version --client 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "kubectl n'est pas installé ou configuré"
exit 1
}
$k8sDir = Join-Path $ProjectRoot "kubernetes"
Write-Info "Création du namespace $Namespace..."
kubectl create namespace $Namespace --dry-run=client -o yaml | kubectl apply -f -
Write-Info "Application des ConfigMaps et Secrets..."
kubectl apply -f (Join-Path $k8sDir "afterwork-configmap.yaml")
if ($LASTEXITCODE -ne 0) {
Write-Warning "ConfigMap déjà existante ou erreur"
}
if (-not $Force) {
Write-Warning "⚠️ ATTENTION : Vérifiez que les secrets sont correctement configurés !"
Write-Warning " Fichier : kubernetes/afterwork-secrets.yaml"
$confirm = Read-Host "Continuer le déploiement? (o/N)"
if ($confirm -ne "o" -and $confirm -ne "O") {
Write-Warning "Déploiement annulé"
exit 0
}
}
kubectl apply -f (Join-Path $k8sDir "afterwork-secrets.yaml")
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de l'application des secrets"
exit 1
}
Write-Info "Déploiement de l'application..."
kubectl apply -f (Join-Path $k8sDir "afterwork-deployment.yaml")
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du déploiement"
exit 1
}
kubectl apply -f (Join-Path $k8sDir "afterwork-service.yaml")
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de la création du service"
exit 1
}
kubectl apply -f (Join-Path $k8sDir "afterwork-ingress.yaml")
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de la création de l'ingress"
exit 1
}
Write-Success "Déploiement Kubernetes réussi"
Write-Info "Attente du rollout..."
kubectl rollout status deployment/$AppName -n $Namespace --timeout=5m
if ($LASTEXITCODE -ne 0) {
Write-Warning "Timeout ou erreur lors du rollout"
}
}
# ======================================================================
# Vérification du déploiement
# ======================================================================
function Verify-Deployment {
Write-Info "[5/5] Vérification du déploiement..."
Write-Info "Pods:"
kubectl get pods -n $Namespace -l app=$AppName
Write-Info "`nService:"
kubectl get svc -n $Namespace $AppName
Write-Info "`nIngress:"
kubectl get ingress -n $Namespace $AppName
Write-Info "`nTest Health Check..."
Start-Sleep -Seconds 5
try {
$response = Invoke-WebRequest -Uri "https://api.lions.dev/afterwork/q/health/ready" -UseBasicParsing -TimeoutSec 10
if ($response.StatusCode -eq 200) {
Write-Success "✓ API accessible : https://api.lions.dev/afterwork"
Write-Success "✓ Health check : OK"
} else {
Write-Warning "⚠ Health check retourné : $($response.StatusCode)"
}
} catch {
Write-Warning "⚠ Impossible de joindre l'API (normal si DNS pas encore propagé)"
Write-Info " Vérifiez manuellement : https://api.lions.dev/afterwork/q/health"
}
Write-Success "`nDéploiement terminé avec succès !"
}
# ======================================================================
# Rollback
# ======================================================================
function Rollback-Deployment {
Write-Warning "Rollback du déploiement..."
kubectl rollout undo deployment/$AppName -n $Namespace
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du rollback"
exit 1
}
kubectl rollout status deployment/$AppName -n $Namespace
Write-Success "Rollback réussi"
}
# ======================================================================
# Status
# ======================================================================
function Get-Status {
Write-Info "Status de $AppName..."
Write-Info "`nPods:"
kubectl get pods -n $Namespace -l app=$AppName
Write-Info "`nDéploiement:"
kubectl get deployment -n $Namespace $AppName
Write-Info "`nService:"
kubectl get svc -n $Namespace $AppName
Write-Info "`nIngress:"
kubectl get ingress -n $Namespace $AppName
Write-Info "`nLogs récents (20 dernières lignes):"
kubectl logs -n $Namespace -l app=$AppName --tail=20
}
# ======================================================================
# Exécution selon l'action
# ======================================================================
switch ($Action) {
"build" {
Build-Application
Build-DockerImage
}
"push" {
Push-ToRegistry
}
"deploy" {
Deploy-ToKubernetes
Verify-Deployment
}
"all" {
Build-Application
Build-DockerImage
Push-ToRegistry
Deploy-ToKubernetes
Verify-Deployment
}
"rollback" {
Rollback-Deployment
}
"status" {
Get-Status
}
}
Write-Info "`n======================================================================"
Write-Info "Terminé!"
Write-Info "======================================================================"

View File

@@ -7,11 +7,11 @@
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/mic-after-work-server-impl-quarkus-main-jvm .
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/mic-after-work-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-server-impl-quarkus-main-jvm
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
@@ -20,7 +20,7 @@
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-server-impl-quarkus-main-jvm
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-jvm
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
@@ -77,7 +77,7 @@
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-17:1.20
FROM registry.access.redhat.com/ubi8/openjdk-17:1.19
ENV LANGUAGE='en_US:en'

View File

@@ -7,11 +7,11 @@
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/mic-after-work-server-impl-quarkus-main-legacy-jar .
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/mic-after-work-legacy-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-server-impl-quarkus-main-legacy-jar
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-legacy-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
@@ -20,7 +20,7 @@
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-server-impl-quarkus-main-legacy-jar
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-legacy-jar
#
# This image uses the `run-java.sh` script to run the application.
# This scripts computes the command line to execute your Java application, and
@@ -77,7 +77,7 @@
# accessed directly. (example: "foo.example.com,bar.example.com")
#
###
FROM registry.access.redhat.com/ubi8/openjdk-17:1.20
FROM registry.access.redhat.com/ubi8/openjdk-17:1.19
ENV LANGUAGE='en_US:en'

View File

@@ -7,14 +7,14 @@
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/mic-after-work-server-impl-quarkus-main .
# docker build -f src/main/docker/Dockerfile.native -t quarkus/mic-after-work .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-server-impl-quarkus-main
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \

View File

@@ -10,11 +10,11 @@
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/mic-after-work-server-impl-quarkus-main .
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/mic-after-work .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-server-impl-quarkus-main
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work
#
###
FROM quay.io/quarkus/quarkus-micro-image:2.0

View File

@@ -0,0 +1,58 @@
version: '3.8'
services:
# Service pour la base de données PostgreSQL
db:
image: postgres:13
container_name: afterwork_db
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5432:5432"
networks:
- afterwork-network
volumes:
- db_data:/var/lib/postgresql/data
# Service pour l'application Quarkus
app:
build:
context: .
dockerfile: src/main/docker/Dockerfile.jvm
container_name: afterwork-quarkus
environment:
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
DB_HOST: db
DB_PORT: 5432
DB_NAME: ${DB_NAME}
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
ports:
- "8080:8080"
depends_on:
- db
networks:
- afterwork-network
# Service pour Swagger UI
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
networks:
afterwork-network:
driver: bridge
volumes:
db_data:
driver: local

View File

@@ -1,44 +0,0 @@
package com.lions.dev.config;
import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.enums.SecuritySchemeType;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.security.SecurityScheme;
import org.eclipse.microprofile.openapi.annotations.servers.Server;
import jakarta.ws.rs.core.Application;
/**
* Configuration OpenAPI pour l'API AfterWork.
*
* Cette classe configure les métadonnées OpenAPI, le serveur de base
* et les schémas de sécurité (JWT Bearer) pour que Swagger UI génère
* correctement les URLs avec le root-path et permette l'authentification.
*/
@OpenAPIDefinition(
info = @Info(
title = "AfterWork API",
version = "1.0.0",
description = "API REST pour l'application AfterWork - Gestion d'événements, réseaux sociaux et messagerie"
),
servers = {
@Server(
url = "https://api.lions.dev/afterwork",
description = "Serveur de production"
),
@Server(
url = "http://localhost:8080",
description = "Serveur de développement local"
)
}
)
@SecurityScheme(
securitySchemeName = "bearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
description = "Authentification JWT. Utilisez le token obtenu via /auth/login"
)
public class OpenAPIConfig extends Application {
// Classe de configuration OpenAPI
}

View File

@@ -1,171 +0,0 @@
package com.lions.dev.config;
import com.lions.dev.entity.events.Events;
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.EventsRepository;
import com.lions.dev.repository.PasswordResetTokenRepository;
import com.lions.dev.repository.StoryRepository;
import com.lions.dev.service.EmailService;
import com.lions.dev.service.NotificationService;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;
/**
* Jobs planifiés (Quarkus Scheduler) pour :
* - Nettoyage des stories expirées (24h)
* - Nettoyage des tokens de reset password expirés
* - Expiration des abonnements établissements
* - Désactivation des établissements non payés
* - Rappels d'événements (J-1, H-1)
*/
@ApplicationScoped
public class ScheduledJobs {
private static final Logger LOG = Logger.getLogger(ScheduledJobs.class);
@Inject
StoryRepository storyRepository;
@Inject
PasswordResetTokenRepository passwordResetTokenRepository;
@Inject
EstablishmentSubscriptionRepository subscriptionRepository;
@Inject
EstablishmentRepository establishmentRepository;
@Inject
EventsRepository eventsRepository;
@Inject
NotificationService notificationService;
@Inject
EmailService emailService;
/** Nettoyage des stories expirées : toutes les heures. */
@Scheduled(cron = "0 0 * * * ?")
@Transactional
public void deactivateExpiredStories() {
int count = storyRepository.deactivateExpiredStories();
if (count > 0) {
LOG.info("[ScheduledJobs] Stories expirées désactivées : " + count);
}
}
/** Nettoyage des tokens de reset password expirés : tous les jours à 3h. */
@Scheduled(cron = "0 0 3 * * ?")
@Transactional
public void deleteExpiredPasswordResetTokens() {
long count = passwordResetTokenRepository.deleteExpiredTokens();
if (count > 0) {
LOG.info("[ScheduledJobs] Tokens de reset password supprimés : " + count);
}
}
/** Expiration des abonnements et désactivation des établissements non payés : toutes les heures. */
@Scheduled(cron = "0 5 * * * ?")
@Transactional
public void expireSubscriptionsAndDisableEstablishments() {
List<EstablishmentSubscription> expired = subscriptionRepository.findExpiredActiveSubscriptions();
for (EstablishmentSubscription sub : expired) {
sub.setStatus(EstablishmentSubscription.STATUS_EXPIRED);
subscriptionRepository.persist(sub);
Establishment est = establishmentRepository.findById(sub.getEstablishmentId());
if (est != null && Boolean.TRUE.equals(est.getIsActive())) {
est.setIsActive(false);
establishmentRepository.persist(est);
LOG.info("[ScheduledJobs] Établissement désactivé (abonnement expiré) : " + est.getId());
}
}
if (!expired.isEmpty()) {
LOG.info("[ScheduledJobs] Abonnements expirés traités : " + expired.size());
}
}
/** Rappels d'événements J-1 (dans ~24h) et H-1 (dans ~1h) : toutes les 15 minutes. */
@Scheduled(cron = "0 */15 * * * ?")
@Transactional
public void sendEventReminders() {
LocalDateTime now = LocalDateTime.now();
// Fenêtre J-1 : début entre 23h30 et 24h30
LocalDateTime j1From = now.plus(23, ChronoUnit.HOURS).plus(30, ChronoUnit.MINUTES);
LocalDateTime j1To = now.plus(24, ChronoUnit.HOURS).plus(30, ChronoUnit.MINUTES);
List<Events> eventsJ1 = eventsRepository.findEventsStartingBetween(j1From, j1To);
for (Events event : eventsJ1) {
sendReminderToParticipants(event, "J-1", "demain");
}
// Fenêtre H-1 : début entre 50 min et 1h10
LocalDateTime h1From = now.plus(50, ChronoUnit.MINUTES);
LocalDateTime h1To = now.plus(70, ChronoUnit.MINUTES);
List<Events> eventsH1 = eventsRepository.findEventsStartingBetween(h1From, h1To);
for (Events event : eventsH1) {
sendReminderToParticipants(event, "H-1", "dans 1 heure");
}
}
/** Avertissement expiration abonnement (J-3) : email au manager. */
@Scheduled(cron = "0 0 9 * * ?")
@Transactional
public void sendSubscriptionExpirationWarningEmails() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime in3DaysStart = now.plusDays(3);
LocalDateTime in3DaysEnd = now.plusDays(3).plusHours(23).plusMinutes(59);
List<EstablishmentSubscription> expiring = subscriptionRepository.findActiveSubscriptionsExpiringBetween(in3DaysStart, in3DaysEnd);
for (EstablishmentSubscription sub : expiring) {
Establishment est = establishmentRepository.findById(sub.getEstablishmentId());
if (est == null) continue;
Users manager = est.getManager();
if (manager == null || manager.getEmail() == null) continue;
try {
emailService.sendSubscriptionExpirationWarningEmail(
manager.getEmail(),
manager.getFirstName(),
est.getName(),
sub.getExpiresAt()
);
} catch (Exception e) {
LOG.warn("[ScheduledJobs] Email expiration abonnement échoué pour " + est.getId() + ": " + e.getMessage());
}
}
if (!expiring.isEmpty()) {
LOG.info("[ScheduledJobs] Emails avertissement expiration envoyés : " + expiring.size());
}
}
private void sendReminderToParticipants(Events event, String reminderType, String whenText) {
Set<Users> participants = event.getParticipants();
if (participants == null) return;
Users creator = event.getCreator();
String title = "Rappel événement " + reminderType + " : " + event.getTitle();
String message = "L'événement « " + event.getTitle() + " » commence " + whenText + ".";
for (Users participant : participants) {
if (participant == null || participant.getId() == null) continue;
try {
notificationService.createNotification(title, message, "reminder", participant.getId(), event.getId());
} catch (Exception e) {
LOG.warn("[ScheduledJobs] Impossible de créer rappel pour participant " + participant.getId() + ": " + e.getMessage());
}
}
if (creator != null && creator.getId() != null && (participants.isEmpty() || !participants.stream().anyMatch(p -> p.getId().equals(creator.getId())))) {
try {
notificationService.createNotification(title, message, "reminder", creator.getId(), event.getId());
} catch (Exception e) {
LOG.warn("[ScheduledJobs] Impossible de créer rappel pour créateur: " + e.getMessage());
}
}
}
}

View File

@@ -1,59 +0,0 @@
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 + ")");
}
}

View File

@@ -0,0 +1,18 @@
package com.lions.dev.core.errors;
/**
* Classe de base pour les exceptions personnalisées dans l'application AfterWork.
* Toutes les exceptions spécifiques peuvent étendre cette classe pour centraliser la gestion des erreurs.
*/
public abstract class Exceptions extends Exception {
/**
* Constructeur de base pour les exceptions personnalisées.
*
* @param message Le message d'erreur associé à l'exception.
*/
public Exceptions(String message) {
super(message);
System.out.println("[ERROR] Exception déclenchée : " + message);
}
}

View File

@@ -15,6 +15,7 @@ public class Failures {
*/
public Failures(String failureMessage) {
this.failureMessage = failureMessage;
System.out.println("[FAILURE] Échec détecté : " + failureMessage);
}
/**

View File

@@ -1,49 +1,35 @@
package com.lions.dev.core.errors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lions.dev.core.errors.exceptions.BadRequestException;
import com.lions.dev.core.errors.exceptions.EventNotFoundException;
import com.lions.dev.core.errors.exceptions.NotFoundException;
import com.lions.dev.core.errors.exceptions.ServerException;
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
import com.lions.dev.exception.EstablishmentHasDependenciesException;
import com.lions.dev.exception.FriendshipNotFoundException;
import com.lions.dev.exception.UserNotFoundException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.util.Collections;
import java.util.Map;
/**
* Gestionnaire global des exceptions pour l'API.
* Ce gestionnaire intercepte les exceptions spécifiques et renvoie des réponses appropriées.
* Les réponses d'erreur sont sérialisées en JSON de façon sûre (pas de concaténation de chaînes).
*/
@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
private static final Logger logger = Logger.getLogger(GlobalExceptionHandler.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Gère les exceptions non traitées et retourne une réponse appropriée.
*
* @param exception L'exception interceptée.
* @return Une réponse HTTP avec un message d'erreur et le code de statut approprié.
*/
@Override
public Response toResponse(Throwable exception) {
if (exception instanceof BadRequestException) {
logger.warn("BadRequestException intercepted: " + exception.getMessage());
return buildResponse(Response.Status.BAD_REQUEST, exception.getMessage());
} else if (exception instanceof UserNotFoundException) {
logger.warn("UserNotFoundException (404): " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
} else if (exception instanceof FriendshipNotFoundException) {
logger.warn("FriendshipNotFoundException (404): " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
} else if (exception instanceof EstablishmentHasDependenciesException) {
logger.warn("EstablishmentHasDependenciesException (409): " + exception.getMessage());
return buildResponse(Response.Status.CONFLICT, exception.getMessage());
} else if (exception instanceof EventNotFoundException || exception instanceof NotFoundException) {
logger.warn("NotFoundException intercepted: " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
@@ -64,18 +50,14 @@ public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
/**
* Crée une réponse HTTP avec un code de statut et un message d'erreur.
* Le message est sérialisé en JSON de façon sûre (échappement automatique).
*
* @param status Le code de statut HTTP.
* @param message Le message d'erreur.
* @return La réponse HTTP formée.
*/
private Response buildResponse(Response.Status status, String message) {
Map<String, String> body = Collections.singletonMap("error", message != null ? message : "");
try {
return Response.status(status)
.type(MediaType.APPLICATION_JSON)
.entity(OBJECT_MAPPER.writeValueAsString(body))
.build();
} catch (JsonProcessingException e) {
logger.error("Impossible de sérialiser la réponse d'erreur", e);
return Response.status(status).type(MediaType.APPLICATION_JSON).entity("{\"error\":\"Erreur serveur\"}").build();
}
return Response.status(status)
.entity("{\"error\":\"" + message + "\"}")
.build();
}
}

View File

@@ -0,0 +1,4 @@
package com.lions.dev.core.errors;
public class ServerException {
}

View File

@@ -16,6 +16,7 @@ public class BadRequestException extends WebApplicationException {
*/
public BadRequestException(String message) {
super(message, Response.Status.BAD_REQUEST);
System.out.println("[ERROR] Requête invalide : " + message);
}
}

View File

@@ -16,5 +16,6 @@ public class NotFoundException extends WebApplicationException {
*/
public NotFoundException(String message) {
super(message, Response.Status.NOT_FOUND);
System.out.println("[ERROR] Ressource non trouvée : " + message);
}
}

View File

@@ -13,5 +13,6 @@ public class ServerException extends RuntimeException {
*/
public ServerException(String message) {
super(message);
System.out.println("[ERROR] Erreur serveur : " + message);
}
}

View File

@@ -16,5 +16,6 @@ public class UnauthorizedException extends WebApplicationException {
*/
public UnauthorizedException(String message) {
super(message, Response.Status.UNAUTHORIZED);
System.out.println("[ERROR] Accès non autorisé : " + message);
}
}

View File

@@ -1,83 +0,0 @@
package com.lions.dev.core.security;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
/**
* Filtre JAX-RS pour l'authentification JWT.
*
* Ce filtre intercepte les requêtes vers les endpoints marqués avec @RequiresAuth
* et vérifie la validité du token JWT.
*
* Le filtre stocke l'ID de l'utilisateur authentifié dans le contexte de la requête
* sous la clé "authenticatedUserId" pour utilisation ultérieure.
*/
@Provider
@RequiresAuth
@Priority(Priorities.AUTHENTICATION)
public class JwtAuthFilter implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(JwtAuthFilter.class);
/**
* Clé utilisée pour stocker l'ID de l'utilisateur authentifié dans le contexte.
*/
public static final String AUTHENTICATED_USER_ID = "authenticatedUserId";
@Inject
JwtValidationService jwtValidationService;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String path = requestContext.getUriInfo().getPath();
String method = requestContext.getMethod();
LOG.debug("[JwtAuthFilter] Vérification de l'authentification pour: " + method + " " + path);
// Récupérer le header Authorization
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authHeader == null || authHeader.isBlank()) {
LOG.warn("[JwtAuthFilter] Token manquant pour: " + method + " " + path);
abortWithUnauthorized(requestContext, "Token d'authentification manquant");
return;
}
// Valider le token et extraire l'userId
Optional<UUID> userIdOpt = jwtValidationService.validateTokenAndGetUserId(authHeader);
if (userIdOpt.isEmpty()) {
LOG.warn("[JwtAuthFilter] Token invalide pour: " + method + " " + path);
abortWithUnauthorized(requestContext, "Token d'authentification invalide ou expiré");
return;
}
// Stocker l'userId dans le contexte pour utilisation ultérieure
UUID authenticatedUserId = userIdOpt.get();
requestContext.setProperty(AUTHENTICATED_USER_ID, authenticatedUserId);
LOG.debug("[JwtAuthFilter] Authentification réussie pour l'utilisateur: " + authenticatedUserId);
}
/**
* Interrompt la requête avec une réponse 401 Unauthorized.
*/
private void abortWithUnauthorized(ContainerRequestContext requestContext, String message) {
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.entity("{\"message\": \"" + message + "\"}")
.type("application/json")
.build()
);
}
}

View File

@@ -1,187 +0,0 @@
package com.lions.dev.core.security;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.UUID;
/**
* Service de validation des tokens JWT.
*
* Ce service valide les tokens JWT HMAC-SHA256 envoyés par les clients et extrait
* l'identifiant de l'utilisateur authentifié.
*
* Utilise une validation manuelle pour supporter HMAC-SHA256 sans dépendance
* sur la configuration complexe de SmallRye JWT.
*/
@ApplicationScoped
public class JwtValidationService {
private static final Logger LOG = Logger.getLogger(JwtValidationService.class);
private static final String ISSUER = "afterwork";
private static final String BEARER_PREFIX = "Bearer ";
private static final ObjectMapper MAPPER = new ObjectMapper();
@ConfigProperty(name = "afterwork.jwt.secret", defaultValue = "afterwork-jwt-secret-min-32-bytes-for-hs256!")
String secret;
/**
* Valide un token JWT et retourne l'ID de l'utilisateur.
*
* @param authorizationHeader Le header Authorization (avec ou sans préfixe "Bearer ")
* @return L'ID de l'utilisateur si le token est valide, Optional.empty() sinon
*/
public Optional<UUID> validateTokenAndGetUserId(String authorizationHeader) {
if (authorizationHeader == null || authorizationHeader.isBlank()) {
LOG.debug("[JwtValidation] Authorization header absent");
return Optional.empty();
}
String token = extractToken(authorizationHeader);
if (token == null || token.isBlank()) {
LOG.debug("[JwtValidation] Token non trouvé dans le header");
return Optional.empty();
}
try {
// Séparer les parties du token
String[] parts = token.split("\\.");
if (parts.length != 3) {
LOG.warn("[JwtValidation] Format de token invalide (attendu: 3 parties)");
return Optional.empty();
}
String headerPart = parts[0];
String payloadPart = parts[1];
String signaturePart = parts[2];
// Vérifier la signature HMAC-SHA256
if (!verifySignature(headerPart, payloadPart, signaturePart)) {
LOG.warn("[JwtValidation] Signature invalide");
return Optional.empty();
}
// Décoder et parser le payload
String payloadJson = new String(Base64.getUrlDecoder().decode(payloadPart), StandardCharsets.UTF_8);
JsonNode payload = MAPPER.readTree(payloadJson);
// Vérifier l'issuer
JsonNode issNode = payload.get("iss");
if (issNode == null || !ISSUER.equals(issNode.asText())) {
LOG.warn("[JwtValidation] Issuer invalide: " + (issNode != null ? issNode.asText() : "null"));
return Optional.empty();
}
// Vérifier l'expiration
JsonNode expNode = payload.get("exp");
if (expNode != null) {
long expiration = expNode.asLong();
long now = System.currentTimeMillis() / 1000;
if (expiration < now) {
LOG.warn("[JwtValidation] Token expiré (exp: " + expiration + ", now: " + now + ")");
return Optional.empty();
}
}
// Extraire le subject (userId)
JsonNode subNode = payload.get("sub");
if (subNode == null || subNode.asText().isBlank()) {
LOG.warn("[JwtValidation] Subject (userId) absent du token");
return Optional.empty();
}
UUID userId = UUID.fromString(subNode.asText());
LOG.debug("[JwtValidation] Token valide pour l'utilisateur: " + userId);
return Optional.of(userId);
} catch (IllegalArgumentException e) {
LOG.warn("[JwtValidation] Subject invalide (pas un UUID): " + e.getMessage());
return Optional.empty();
} catch (Exception e) {
LOG.error("[JwtValidation] Erreur lors de la validation du token: " + e.getMessage(), e);
return Optional.empty();
}
}
/**
* Vérifie la signature HMAC-SHA256 du token.
*/
private boolean verifySignature(String header, String payload, String signature) {
try {
SecretKey key = getSecretKey();
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
String dataToSign = header + "." + payload;
byte[] expectedSignature = mac.doFinal(dataToSign.getBytes(StandardCharsets.UTF_8));
String expectedSignatureBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(expectedSignature);
return expectedSignatureBase64.equals(signature);
} catch (Exception e) {
LOG.error("[JwtValidation] Erreur lors de la vérification de la signature: " + e.getMessage());
return false;
}
}
/**
* Vérifie si le token appartient à l'utilisateur spécifié.
*
* @param authorizationHeader Le header Authorization
* @param expectedUserId L'ID de l'utilisateur attendu
* @return true si le token appartient à cet utilisateur
*/
public boolean isTokenOwner(String authorizationHeader, UUID expectedUserId) {
if (expectedUserId == null) {
return false;
}
Optional<UUID> tokenUserId = validateTokenAndGetUserId(authorizationHeader);
return tokenUserId.isPresent() && tokenUserId.get().equals(expectedUserId);
}
/**
* Vérifie si le token est valide sans retourner l'utilisateur.
*
* @param authorizationHeader Le header Authorization
* @return true si le token est valide
*/
public boolean isValidToken(String authorizationHeader) {
return validateTokenAndGetUserId(authorizationHeader).isPresent();
}
/**
* Extrait le token du header Authorization.
*
* @param authorizationHeader Le header complet
* @return Le token sans le préfixe "Bearer ", ou null si invalide
*/
private String extractToken(String authorizationHeader) {
if (authorizationHeader.startsWith(BEARER_PREFIX)) {
return authorizationHeader.substring(BEARER_PREFIX.length()).trim();
}
// Si pas de préfixe, retourner tel quel (pour compatibilité)
return authorizationHeader.trim();
}
/**
* Génère la clé secrète à partir de la configuration.
*/
private SecretKey getSecretKey() {
byte[] decoded = secret.getBytes(StandardCharsets.UTF_8);
if (decoded.length < 32) {
byte[] padded = new byte[32];
System.arraycopy(decoded, 0, padded, 0, decoded.length);
decoded = padded;
}
return new SecretKeySpec(decoded, "HmacSHA256");
}
}

View File

@@ -1,33 +0,0 @@
package com.lions.dev.core.security;
import jakarta.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation pour marquer les endpoints qui nécessitent une authentification JWT.
*
* Lorsque cette annotation est présente sur une méthode ou une classe,
* le filtre {@link JwtAuthFilter} vérifiera la présence et la validité
* du token JWT dans le header Authorization.
*
* Usage:
* <pre>
* @RequiresAuth
* @POST
* public Response createPost(...) { ... }
* </pre>
*/
@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequiresAuth {
/**
* Si true, vérifie que l'utilisateur du token correspond au userId de la requête.
* Par défaut, seule la validité du token est vérifiée.
*/
boolean verifyOwnership() default false;
}

View File

@@ -1,19 +0,0 @@
package com.lions.dev.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class PasswordResetRequest {
@NotBlank(message = "L'email est obligatoire")
@Email(message = "Format d'email invalide")
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

View File

@@ -1,75 +0,0 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
/**
* Événement de message chat publié dans Kafka.
*
* Utilisé pour garantir la livraison des messages même si le destinataire
* est temporairement déconnecté. Le message est persisté dans Kafka et
* délivré dès la reconnexion.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessageEvent {
/**
* ID de la conversation (utilisé comme clé Kafka pour garantir l'ordre).
*/
private String conversationId;
/**
* ID de l'expéditeur.
*/
private String senderId;
/**
* ID du destinataire.
*/
private String recipientId;
/**
* Contenu du message.
*/
private String content;
/**
* ID unique du message.
*/
private String messageId;
/**
* Timestamp de création.
*/
private Long timestamp;
/**
* Type d'événement (message, typing, read_receipt, delivery_confirmation).
*/
private String eventType;
/**
* Données additionnelles (pour typing indicators, read receipts, etc.).
*/
private java.util.Map<String, Object> metadata;
/**
* Constructeur pour un message standard.
*/
public ChatMessageEvent(String conversationId, String senderId, String recipientId,
String content, String messageId) {
this.conversationId = conversationId;
this.senderId = senderId;
this.recipientId = recipientId;
this.content = content;
this.messageId = messageId;
this.eventType = "message";
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -1,52 +0,0 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
import java.util.UUID;
/**
* Événement de notification publié dans Kafka.
*
* Utilisé pour découpler les services métier des WebSockets.
* Les services publient dans Kafka, et un bridge consomme depuis Kafka
* pour envoyer via WebSocket aux clients connectés.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class NotificationEvent {
/**
* ID de l'utilisateur destinataire (utilisé comme clé Kafka pour routing).
*/
private String userId;
/**
* Type de notification (friend_request, friend_request_accepted, event_reminder, etc.).
*/
private String type;
/**
* Données de la notification (contenu spécifique au type).
*/
private Map<String, Object> data;
/**
* Timestamp de création de l'événement.
*/
private Long timestamp;
/**
* Constructeur simplifié (timestamp auto-généré).
*/
public NotificationEvent(String userId, String type, Map<String, Object> data) {
this.userId = userId;
this.type = type;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -1,48 +0,0 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Événement de présence (online/offline) publié dans Kafka.
*
* Utilisé pour notifier les amis quand un utilisateur se connecte/déconnecte.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PresenceEvent {
/**
* ID de l'utilisateur concerné.
*/
private String userId;
/**
* Statut (online, offline).
*/
private String status;
/**
* Timestamp de dernière activité.
*/
private Long lastSeen;
/**
* Timestamp de l'événement.
*/
private Long timestamp;
/**
* Constructeur simplifié.
*/
public PresenceEvent(String userId, String status, Long lastSeen) {
this.userId = userId;
this.status = status;
this.lastSeen = lastSeen;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -1,63 +0,0 @@
package com.lions.dev.dto.events;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
/**
* Événement de réaction (like, comment, share) publié dans Kafka.
*
* Utilisé pour notifier en temps réel les réactions sur les posts,
* stories et événements.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReactionEvent {
/**
* ID du post/story/event concerné (utilisé comme clé Kafka).
*/
private String targetId;
/**
* Type de cible (post, story, event).
*/
private String targetType;
/**
* ID de l'utilisateur qui réagit.
*/
private String userId;
/**
* Type de réaction (like, comment, share).
*/
private String reactionType;
/**
* Données additionnelles (contenu du commentaire, etc.).
*/
private Map<String, Object> data;
/**
* Timestamp de création.
*/
private Long timestamp;
/**
* Constructeur simplifié.
*/
public ReactionEvent(String targetId, String targetType, String userId,
String reactionType, Map<String, Object> data) {
this.targetId = targetId;
this.targetType = targetType;
this.userId = userId;
this.reactionType = reactionType;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -1,66 +0,0 @@
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;
}
}

View File

@@ -1,31 +0,0 @@
package com.lions.dev.dto.request.chat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
/**
* DTO pour l'envoi d'un message.
* Validation déclarative via Bean Validation (Hibernate Validator).
*/
@Getter
@Setter
@NoArgsConstructor
public class SendMessageRequestDTO {
@NotNull(message = "L'ID de l'expéditeur est obligatoire")
private UUID senderId;
@NotNull(message = "L'ID du destinataire est obligatoire")
private UUID recipientId;
@NotBlank(message = "Le contenu du message est obligatoire")
private String content;
private String messageType; // text, image, video, file (optionnel, défaut text)
private String mediaUrl; // optionnel
}

View File

@@ -1,85 +0,0 @@
package com.lions.dev.dto.request.establishment;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
/**
* DTO pour la création d'un établissement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Seuls les responsables d'établissement peuvent créer des établissements.
*/
@Getter
@Setter
public class EstablishmentCreateRequestDTO {
@NotNull(message = "Le nom de l'établissement est obligatoire.")
@Size(min = 2, max = 200, message = "Le nom doit comporter entre 2 et 200 caractères.")
private String name;
@NotNull(message = "Le type d'établissement est obligatoire.")
private String type;
@NotNull(message = "L'adresse est obligatoire.")
private String address;
@NotNull(message = "La ville est obligatoire.")
private String city;
@NotNull(message = "Le code postal est obligatoire.")
private String postalCode;
private String description;
private String phoneNumber;
private String website;
private String priceRange;
private String verificationStatus = "PENDING"; // v2.0 - Par défaut PENDING
private Double latitude;
private Double longitude;
@NotNull(message = "L'identifiant du responsable est obligatoire.")
private UUID managerId;
// Champs dépréciés (v1.0) - conservés pour compatibilité mais ignorés
/**
* @deprecated Supprimé en v2.0 (utiliser manager.email à la place).
*/
@Deprecated
private String email;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_media à la place).
*/
@Deprecated
private String imageUrl;
/**
* @deprecated Utiliser averageRating calculé depuis reviews à la place.
*/
@Deprecated
private Double rating;
/**
* @deprecated Supprimé en v2.0.
*/
@Deprecated
private Integer capacity;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_amenities à la place).
*/
@Deprecated
private String amenities;
/**
* @deprecated Supprimé en v2.0 (utiliser business_hours à la place).
*/
@Deprecated
private String openingHours;
}

View File

@@ -1,31 +0,0 @@
package com.lions.dev.dto.request.establishment;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la requête d'upload d'un média d'établissement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
*/
@Getter
@Setter
public class EstablishmentMediaRequestDTO {
@NotBlank(message = "L'URL du média est obligatoire")
private String mediaUrl;
@NotBlank(message = "Le type de média est obligatoire")
private String mediaType; // PHOTO ou VIDEO
private String name; // Nom du fichier (fileName) - optionnel, peut être extrait de mediaUrl si non fourni
private String thumbnailUrl; // Optionnel, pour les vidéos
private Integer displayOrder = 0; // Ordre d'affichage (par défaut 0)
private String uploadedByUserId; // ID de l'utilisateur qui upload (optionnel, peut être extrait du contexte)
}

View File

@@ -1,25 +0,0 @@
package com.lions.dev.dto.request.establishment;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour soumettre ou modifier une note d'établissement.
*/
@Getter
@Setter
public class EstablishmentRatingRequestDTO {
@NotNull(message = "La note est obligatoire.")
@Min(value = 1, message = "La note doit être au moins 1 étoile.")
@Max(value = 5, message = "La note ne peut pas dépasser 5 étoiles.")
private Integer rating; // Note de 1 à 5
@Size(max = 2000, message = "Le commentaire ne peut pas dépasser 2000 caractères.")
private String comment; // Commentaire optionnel
}

View File

@@ -1,34 +0,0 @@
package com.lions.dev.dto.request.establishment;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la mise à jour d'un établissement.
*/
@Getter
@Setter
public class EstablishmentUpdateRequestDTO {
@Size(min = 2, max = 200, message = "Le nom doit comporter entre 2 et 200 caractères.")
private String name;
private String type;
private String address;
private String city;
private String postalCode;
private String description;
private String phoneNumber;
private String email;
private String website;
private String imageUrl;
private Double rating;
private String priceRange;
private Integer capacity;
private String amenities;
private String openingHours;
private Double latitude;
private Double longitude;
}

View File

@@ -1,27 +0,0 @@
package com.lions.dev.dto.request.establishment;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
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")
@Size(max = 25, message = "Le numéro de téléphone ne peut pas dépasser 25 caractères")
private String clientPhone;
}

View File

@@ -6,14 +6,9 @@ import lombok.Setter;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import org.jboss.logging.Logger;
/**
* DTO pour la création d'un événement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé dans les requêtes de création d'événements, envoyant les informations
* nécessaires comme le titre, les dates, la description, le créateur, et d'autres attributs.
*/
@@ -33,44 +28,15 @@ public class EventCreateRequestDTO {
@NotNull(message = "La date de fin est obligatoire.")
private LocalDateTime endDate; // Date de fin de l'événement
private UUID establishmentId; // v2.0 - ID de l'établissement où se déroule l'événement
private String location; // Lieu de l'événement
private String category; // Catégorie de l'événement
private String link; // Lien d'information supplémentaire
private String imageUrl; // URL de l'image associée à l'événement
private Integer maxParticipants; // Nombre maximum de participants autorisés
private String tags; // Tags/mots-clés associés à l'événement (séparés par des virgules)
private String organizer; // Nom de l'organisateur de l'événement
private Integer participationFee; // Frais de participation en centimes
private Boolean isPrivate = false; // v2.0 - Indique si l'événement est privé
private Boolean waitlistEnabled = false; // v2.0 - Indique si la liste d'attente est activée
private String privacyRules; // Règles de confidentialité de l'événement
private String transportInfo; // Informations sur les transports disponibles
private String accommodationInfo; // Informations sur l'hébergement
private String accessibilityInfo; // Informations sur l'accessibilité
private String parkingInfo; // Informations sur le parking
private String securityProtocol; // Protocole de sécurité de l'événement
@NotNull(message = "L'identifiant du créateur est obligatoire.")
private UUID creatorId; // Identifiant du créateur de l'événement
// Champ déprécié (v1.0) - conservé pour compatibilité mais ignoré
/**
* @deprecated Supprimé en v2.0 (utiliser establishmentId à la place).
*/
@Deprecated
private String location;
public EventCreateRequestDTO() {
}
/**
* Méthode pour obtenir le lieu (compatibilité v1.0 et v2.0).
* Retourne null car location est déprécié en v2.0.
*
* @return Le lieu (null en v2.0, utiliser establishmentId à la place).
*/
public String getLocation() {
return location; // Retourne null en v2.0
System.out.println("[LOG] DTO de requête de création d'événement initialisé.");
}
}

View File

@@ -2,7 +2,6 @@ package com.lions.dev.dto.request.events;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
import org.jboss.logging.Logger;
/**
* DTO pour la suppression d'un événement.
@@ -16,5 +15,6 @@ public class EventDeleteRequestDTO {
private UUID eventId; // ID de l'événement à supprimer
public EventDeleteRequestDTO() {
System.out.println("[LOG] DTO de requête de suppression d'événement initialisé.");
}
}

View File

@@ -16,10 +16,7 @@ import lombok.Setter;
@AllArgsConstructor
public class EventReadManyByIdRequestDTO {
private UUID id; // v2.0 - Identifiant de l'utilisateur pour lequel on souhaite obtenir les événements
private Integer page = 0; // v2.0 - Numéro de la page (0-indexé)
private Integer size = 10; // v2.0 - Taille de la page
private UUID userId; // Identifiant de l'utilisateur pour lequel on souhaite obtenir les événements
// Ajoutez ici d'autres critères de filtre si besoin, comme une plage de dates, un statut, etc.
}

View File

@@ -2,7 +2,6 @@ package com.lions.dev.dto.request.events;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
import org.jboss.logging.Logger;
/**
* DTO pour lire un événement par son ID.
@@ -16,5 +15,6 @@ public class EventReadOneByIdRequestDTO {
private UUID eventId; // ID de l'événement à lire
public EventReadOneByIdRequestDTO() {
System.out.println("[LOG] DTO de requête de lecture d'événement initialisé.");
}
}

View File

@@ -1,6 +1,5 @@
package com.lions.dev.dto.request.friends;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
@@ -16,10 +15,7 @@ import java.util.UUID;
@NoArgsConstructor
public class FriendshipCreateOneRequestDTO {
@NotNull(message = "L'identifiant de l'utilisateur est requis")
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
/**

View File

@@ -1,49 +0,0 @@
package com.lions.dev.dto.request.promotion;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour la création d'une promotion.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PromotionCreateRequestDTO {
@NotNull(message = "L'ID de l'établissement est obligatoire")
private UUID establishmentId;
@NotBlank(message = "Le titre est obligatoire")
@Size(max = 200, message = "Le titre ne peut pas dépasser 200 caractères")
private String title;
private String description;
@Size(max = 50, message = "Le code promo ne peut pas dépasser 50 caractères")
private String promoCode;
@NotBlank(message = "Le type de réduction est obligatoire")
private String discountType; // PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
@NotNull(message = "La valeur de réduction est obligatoire")
@Positive(message = "La valeur de réduction doit être positive")
private BigDecimal discountValue;
@NotNull(message = "La date de début est obligatoire")
private LocalDateTime validFrom;
@NotNull(message = "La date de fin est obligatoire")
private LocalDateTime validUntil;
}

View File

@@ -1,41 +0,0 @@
package com.lions.dev.dto.request.promotion;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* DTO pour la mise à jour d'une promotion.
* Tous les champs sont optionnels.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PromotionUpdateRequestDTO {
@Size(max = 200, message = "Le titre ne peut pas dépasser 200 caractères")
private String title;
private String description;
@Size(max = 50, message = "Le code promo ne peut pas dépasser 50 caractères")
private String promoCode;
private String discountType; // PERCENTAGE, FIXED_AMOUNT, FREE_ITEM
@Positive(message = "La valeur de réduction doit être positive")
private BigDecimal discountValue;
private LocalDateTime validFrom;
private LocalDateTime validUntil;
private Boolean isActive;
}

View File

@@ -1,37 +0,0 @@
package com.lions.dev.dto.request.review;
import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
import java.util.UUID;
/**
* DTO pour la création d'un avis sur un établissement.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReviewCreateRequestDTO {
@NotNull(message = "L'ID de l'établissement est obligatoire")
private UUID establishmentId;
@NotNull(message = "La note globale est obligatoire")
@Min(value = 1, message = "La note doit être au minimum 1")
@Max(value = 5, message = "La note doit être au maximum 5")
private Integer overallRating;
@Size(max = 2000, message = "Le commentaire ne peut pas dépasser 2000 caractères")
private String comment;
/**
* Notes par critères (optionnel).
* Clés possibles: "ambiance", "service", "qualite", "rapport_qualite_prix", "proprete"
*/
private Map<String, Integer> criteriaRatings;
}

View File

@@ -1,34 +0,0 @@
package com.lions.dev.dto.request.review;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Map;
/**
* DTO pour la mise à jour d'un avis.
* Tous les champs sont optionnels.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReviewUpdateRequestDTO {
@Min(value = 1, message = "La note doit être au minimum 1")
@Max(value = 5, message = "La note doit être au maximum 5")
private Integer overallRating;
@Size(max = 2000, message = "Le commentaire ne peut pas dépasser 2000 caractères")
private String comment;
/**
* Notes par critères (optionnel).
*/
private Map<String, Integer> criteriaRatings;
}

View File

@@ -1,30 +0,0 @@
package com.lions.dev.dto.request.social;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
/**
* DTO (Data Transfer Object) pour la création d'un commentaire sur un post social.
*
* Valide que le contenu n'est pas vide et ne dépasse pas 1000 caractères.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PostCommentCreateRequestDTO {
@NotBlank(message = "Le contenu du commentaire est obligatoire")
@Size(max = 1000, message = "Le commentaire ne peut pas dépasser 1000 caractères")
private String content;
@NotNull(message = "L'identifiant de l'utilisateur est obligatoire")
private UUID userId;
}

View File

@@ -1,35 +0,0 @@
package com.lions.dev.dto.request.social;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.jboss.logging.Logger;
/**
* DTO pour la création d'un post social.
*
* Ce DTO est utilisé dans les requêtes de création de posts sociaux,
* envoyant les informations nécessaires comme le contenu, l'utilisateur
* créateur, et optionnellement une image.
*/
@Getter
@Setter
public class SocialPostCreateRequestDTO {
@NotBlank(message = "Le contenu du post est obligatoire.")
@Size(max = 2000, message = "Le contenu ne peut pas dépasser 2000 caractères.")
private String content; // Le contenu textuel du post
@NotNull(message = "L'identifiant de l'utilisateur est obligatoire.")
private UUID creatorId; // v2.0 - L'ID de l'utilisateur créateur
@Size(max = 500, message = "L'URL de l'image ne peut pas dépasser 500 caractères.")
private String imageUrl; // URL de l'image (optionnel)
public SocialPostCreateRequestDTO() {
}
}

View File

@@ -1,25 +0,0 @@
package com.lions.dev.dto.request.social;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la mise à jour d'un post social.
*
* Utilisé dans les requêtes PUT /posts/{id} avec un body JSON
* (content, imageUrl) envoyé par le client Flutter.
*/
@Getter
@Setter
public class SocialPostUpdateRequestDTO {
@Size(max = 2000, message = "Le contenu ne peut pas dépasser 2000 caractères.")
private String content;
@Size(max = 500, message = "L'URL de l'image ne peut pas dépasser 500 caractères.")
private String imageUrl;
public SocialPostUpdateRequestDTO() {
}
}

View File

@@ -1,39 +0,0 @@
package com.lions.dev.dto.request.story;
import com.lions.dev.entity.story.MediaType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.jboss.logging.Logger;
/**
* DTO pour la création d'une story.
*
* Ce DTO est utilisé dans les requêtes de création de stories,
* envoyant les informations nécessaires comme le média, le type et l'utilisateur.
*/
@Getter
@Setter
public class StoryCreateRequestDTO {
@NotNull(message = "L'identifiant de l'utilisateur est obligatoire.")
private UUID creatorId; // v2.0 - L'ID de l'utilisateur créateur
@NotNull(message = "Le type de média est obligatoire.")
private MediaType mediaType; // Type de média (IMAGE ou VIDEO)
@NotBlank(message = "L'URL du média est obligatoire.")
@Size(max = 500, message = "L'URL du média ne peut pas dépasser 500 caractères.")
private String mediaUrl; // URL du média
@Size(max = 500, message = "L'URL du thumbnail ne peut pas dépasser 500 caractères.")
private String thumbnailUrl; // URL du thumbnail (optionnel, pour les vidéos)
private Integer durationSeconds; // Durée en secondes (optionnel, pour les vidéos)
public StoryCreateRequestDTO() {
}
}

View File

@@ -1,26 +0,0 @@
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;
}
}

View File

@@ -1,22 +0,0 @@
package com.lions.dev.dto.request.users;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO pour forcer l'activation ou la suspension d'un utilisateur (opération réservée au super admin).
*/
@Getter
@Setter
@NoArgsConstructor
public class SetUserActiveRequestDTO {
@NotNull(message = "Le champ active est obligatoire")
private Boolean active;
public SetUserActiveRequestDTO(Boolean active) {
this.active = active;
}
}

View File

@@ -1,24 +0,0 @@
package com.lions.dev.dto.request.users;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* DTO pour la mise à jour de l'image de profil (URL après upload).
* Le client envoie l'URL retournée par l'endpoint d'upload de médias.
* Accepte profile_image_url (snake_case) ou profileImageUrl (camelCase).
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateProfileImageRequestDTO {
@NotBlank(message = "L'URL de l'image de profil est obligatoire.")
@JsonProperty("profile_image_url")
private String profileImageUrl;
}

View File

@@ -9,10 +9,6 @@ import org.slf4j.LoggerFactory;
/**
* DTO pour la requête d'authentification de l'utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Utilisé pour encapsuler les informations nécessaires lors de l'authentification d'un utilisateur.
*/
@Getter
@@ -29,16 +25,9 @@ public class UserAuthenticateRequestDTO {
private String email;
/**
* Mot de passe hashé de l'utilisateur (v2.0).
* Format standardisé pour l'authentification.
* Mot de passe de l'utilisateur en texte clair.
* Ce champ sera haché avant d'être utilisé pour l'authentification.
*/
private String password_hash; // v2.0
/**
* Mot de passe de l'utilisateur en texte clair (v1.0 - déprécié).
* @deprecated Utiliser {@link #password_hash} à la place.
*/
@Deprecated
private String motDePasse;
/**
@@ -48,15 +37,6 @@ public class UserAuthenticateRequestDTO {
logger.info("UserAuthenticateRequestDTO - DTO pour l'authentification initialisé");
}
/**
* Méthode pour obtenir le mot de passe (compatibilité v1.0 et v2.0).
*
* @return Le mot de passe (password_hash ou motDePasse).
*/
public String getPassword() {
return password_hash != null ? password_hash : motDePasse;
}
// Méthode personnalisée pour loguer les détails de la requête
public void logRequestDetails() {
logger.info("Authentification demandée pour l'email: {}", email);

View File

@@ -7,25 +7,21 @@ import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la création d'un utilisateur.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé dans les requêtes pour créer un utilisateur,
* contenant les informations comme le prénom, le nom, l'email, et le mot de passe.
* DTO pour la création et l'authentification d'un utilisateur.
* Ce DTO est utilisé dans les requêtes pour créer ou authentifier un utilisateur,
* contenant les informations comme le nom, les prénoms, l'email, et le mot de passe.
*/
@Getter
@Setter
public class UserCreateRequestDTO {
@NotNull(message = "Le prénom est obligatoire.")
@Size(min = 1, max = 100, message = "Le prénom doit comporter entre 1 et 100 caractères.")
private String firstName; // v2.0
@NotNull(message = "Le nom est obligatoire.")
@Size(min = 1, max = 100, message = "Le nom doit comporter entre 1 et 100 caractères.")
private String nom;
@NotNull(message = "Le nom de famille est obligatoire.")
@Size(min = 1, max = 100, message = "Le nom de famille doit comporter entre 1 et 100 caractères.")
private String lastName; // v2.0
@NotNull(message = "Les prénoms sont obligatoires.")
@Size(min = 1, max = 100, message = "Les prénoms doivent comporter entre 1 et 100 caractères.")
private String prenoms;
@NotNull(message = "L'adresse email est obligatoire.")
@Email(message = "Veuillez fournir une adresse email valide.")
@@ -33,86 +29,11 @@ public class UserCreateRequestDTO {
@NotNull(message = "Le mot de passe est obligatoire.")
@Size(min = 6, message = "Le mot de passe doit comporter au moins 6 caractères.")
private String password; // v2.0 - sera hashé en passwordHash
private String motDePasse;
private String profileImageUrl;
private String bio; // v2.0
private Integer loyaltyPoints = 0; // v2.0
/**
* Préférences utilisateur (v2.0).
*
* Structure attendue:
* {
* "preferredCategory": "RESTAURANT" | "BAR" | "CLUB" | "CAFE" | "EVENT" | null,
* "notifications": {
* "email": boolean,
* "push": boolean
* },
* "language": "fr" | "en" | "es"
* }
*
* Exemple:
* {
* "preferredCategory": "RESTAURANT",
* "notifications": {
* "email": true,
* "push": true
* },
* "language": "fr"
* }
*/
private java.util.Map<String, Object> preferences; // v2.0
// Ajout du rôle avec validation
@NotNull(message = "Le rôle est obligatoire.")
private String role; // Rôle de l'utilisateur (par exemple : ADMIN, USER, MANAGER, etc.)
// Champs de compatibilité v1.0 (dépréciés mais supportés pour migration progressive)
/**
* @deprecated Utiliser {@link #firstName} à la place.
*/
@Deprecated
private String prenoms;
/**
* @deprecated Utiliser {@link #lastName} à la place.
*/
@Deprecated
private String nom;
/**
* @deprecated Utiliser {@link #password} à la place.
*/
@Deprecated
private String motDePasse;
/**
* Méthode pour obtenir le prénom (compatibilité v1.0 et v2.0).
*
* @return Le prénom (firstName ou prenoms).
*/
public String getFirstName() {
return firstName != null ? firstName : prenoms;
}
/**
* Méthode pour obtenir le nom de famille (compatibilité v1.0 et v2.0).
*
* @return Le nom de famille (lastName ou nom).
*/
public String getLastName() {
return lastName != null ? lastName : nom;
}
/**
* Méthode pour obtenir le mot de passe (compatibilité v1.0 et v2.0).
*
* @return Le mot de passe (password ou motDePasse).
*/
public String getPassword() {
return password != null ? password : motDePasse;
}
private String role; // Rôle de l'utilisateur (par exemple : ADMIN, USER, etc.)
}

View File

@@ -1,20 +0,0 @@
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;
}

View File

@@ -1,26 +0,0 @@
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;
}

View File

@@ -1,45 +0,0 @@
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;
}
}

View File

@@ -1,57 +0,0 @@
package com.lions.dev.dto.response.chat;
import com.lions.dev.entity.chat.Conversation;
import com.lions.dev.entity.users.Users;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO de réponse pour une conversation.
*/
@Getter
@Setter
@NoArgsConstructor
public class ConversationResponseDTO {
private UUID id;
private UUID participantId;
private String participantFirstName;
private String participantLastName;
private String participantProfileImageUrl;
private String lastMessage;
private LocalDateTime lastMessageTimestamp;
private int unreadCount;
private boolean isTyping;
/** Indique si le participant (l'autre utilisateur) est actuellement en ligne (WebSocket notifications). */
private boolean participantIsOnline;
/**
* Constructeur depuis une entité Conversation.
*
* @param conversation La conversation
* @param currentUser L'utilisateur actuel (pour déterminer l'autre utilisateur)
*/
public ConversationResponseDTO(Conversation conversation, Users currentUser) {
this.id = conversation.getId();
// Déterminer l'autre utilisateur
Users otherUser = conversation.getOtherUser(currentUser);
if (otherUser != null) {
this.participantId = otherUser.getId();
// v2.0 - Utiliser les nouveaux noms de champs
this.participantFirstName = otherUser.getFirstName();
this.participantLastName = otherUser.getLastName();
this.participantProfileImageUrl = otherUser.getProfileImageUrl();
this.participantIsOnline = otherUser.isOnline();
}
this.lastMessage = conversation.getLastMessageContent();
this.lastMessageTimestamp = conversation.getLastMessageTimestamp();
this.unreadCount = conversation.getUnreadCountForUser(currentUser);
this.isTyping = false; // Par défaut, pas en train de taper
}
}

View File

@@ -1,50 +0,0 @@
package com.lions.dev.dto.response.chat;
import com.lions.dev.entity.chat.Message;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO de réponse pour un message.
*/
@Getter
@Setter
@NoArgsConstructor
public class MessageResponseDTO {
private UUID id;
private UUID conversationId;
private UUID senderId;
private String senderFirstName;
private String senderLastName;
private String senderProfileImageUrl;
private String content;
private String attachmentType;
private String attachmentUrl;
private boolean isRead;
private boolean isDelivered;
private LocalDateTime timestamp;
/**
* Constructeur depuis une entité Message (v2.0).
*/
public MessageResponseDTO(Message message) {
this.id = message.getId();
this.conversationId = message.getConversation().getId();
this.senderId = message.getSender().getId();
// v2.0 - Utiliser les nouveaux noms de champs
this.senderFirstName = message.getSender().getFirstName();
this.senderLastName = message.getSender().getLastName();
this.senderProfileImageUrl = message.getSender().getProfileImageUrl();
this.content = message.getContent();
this.attachmentType = message.getMessageType();
this.attachmentUrl = message.getMediaUrl();
this.isRead = message.isRead();
this.isDelivered = message.isDelivered();
this.timestamp = message.getCreatedAt();
}
}

View File

@@ -64,9 +64,8 @@ public class CommentResponseDTO {
this.id = comment.getId(); // Identifiant unique du commentaire
this.texte = comment.getText(); // Texte du commentaire
this.userId = comment.getUser().getId(); // Identifiant de l'utilisateur (auteur du commentaire)
// v2.0 - Utiliser les nouveaux noms de champs
this.userNom = comment.getUser().getLastName(); // Nom de famille de l'utilisateur (v2.0)
this.userPrenoms = comment.getUser().getFirstName(); // Prénom de l'utilisateur (v2.0)
this.userNom = comment.getUser().getNom(); // Nom de l'utilisateur
this.userPrenoms = comment.getUser().getPrenoms(); // Prénom de l'utilisateur
}
}
}

View File

@@ -1,46 +0,0 @@
package com.lions.dev.dto.response.establishment;
import com.lions.dev.entity.establishment.BusinessHours;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour renvoyer les horaires d'ouverture d'un établissement.
* Conforme à l'architecture AfterWork v2.0.
*/
@Getter
public class BusinessHoursResponseDTO {
private String id;
private String establishmentId;
private String dayOfWeek;
private String openTime;
private String closeTime;
private Boolean isClosed;
private Boolean isException;
private LocalDateTime exceptionDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur qui transforme une entité BusinessHours en DTO.
* Utilise establishmentId fourni pour éviter LazyInitializationException.
*
* @param businessHours L'entité à convertir.
* @param establishmentId ID de l'établissement (déjà connu par l'appelant).
*/
public BusinessHoursResponseDTO(BusinessHours businessHours, UUID establishmentId) {
this.id = businessHours.getId() != null ? businessHours.getId().toString() : null;
this.establishmentId = establishmentId != null ? establishmentId.toString() : null;
this.dayOfWeek = businessHours.getDayOfWeek();
this.openTime = businessHours.getOpenTime();
this.closeTime = businessHours.getCloseTime();
this.isClosed = businessHours.getIsClosed();
this.isException = businessHours.getIsException();
this.exceptionDate = businessHours.getExceptionDate();
this.createdAt = businessHours.getCreatedAt();
this.updatedAt = businessHours.getUpdatedAt();
}
}

View File

@@ -1,44 +0,0 @@
package com.lions.dev.dto.response.establishment;
import com.lions.dev.entity.establishment.EstablishmentAmenity;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour renvoyer un équipement d'établissement (avec nom du type).
* Conforme à l'architecture AfterWork v2.0.
* Utilise des paramètres explicites pour éviter LazyInitializationException.
*/
@Getter
public class EstablishmentAmenityResponseDTO {
private String establishmentId;
private String amenityId;
private String amenityName;
private String category;
private String icon;
private String details;
private LocalDateTime createdAt;
/**
* Constructeur qui transforme une entité EstablishmentAmenity en DTO.
* Les champs du type (name, category, icon) sont passés en paramètres car ils peuvent
* provenir d'un JOIN FETCH déjà résolu ou être null si le type n'est pas chargé.
*
* @param ea L'entité à convertir.
* @param amenityName Nom du type d'équipement (ex: "WiFi", "Parking").
* @param category Catégorie du type (ex: "Comfort", "Accessibility").
* @param icon Nom de l'icône (ex: "wifi", "parking").
*/
public EstablishmentAmenityResponseDTO(EstablishmentAmenity ea, String amenityName, String category, String icon) {
this.establishmentId = ea.getEstablishmentId() != null ? ea.getEstablishmentId().toString() : null;
this.amenityId = ea.getAmenityId() != null ? ea.getAmenityId().toString() : null;
this.amenityName = amenityName;
this.category = category;
this.icon = icon;
this.details = ea.getDetails();
this.createdAt = ea.getCreatedAt();
}
}

View File

@@ -1,67 +0,0 @@
package com.lions.dev.dto.response.establishment;
import com.lions.dev.entity.establishment.EstablishmentMedia;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour renvoyer les informations d'un média d'établissement.
*/
@Getter
public class EstablishmentMediaResponseDTO {
private String id;
private String establishmentId;
private String mediaUrl;
private String mediaType; // "PHOTO" ou "VIDEO"
private String thumbnailUrl;
private MediaUploaderDTO uploadedBy;
private LocalDateTime uploadedAt;
private Integer displayOrder;
/**
* Constructeur qui transforme une entité EstablishmentMedia en DTO.
*
* @param media Le média à convertir en DTO.
*/
public EstablishmentMediaResponseDTO(EstablishmentMedia media) {
this.id = media.getId().toString();
this.establishmentId = media.getEstablishment().getId().toString();
this.mediaUrl = media.getMediaUrl();
this.mediaType = media.getMediaType().name();
this.thumbnailUrl = media.getThumbnailUrl();
this.uploadedAt = media.getUploadedAt();
this.displayOrder = media.getDisplayOrder();
if (media.getUploadedBy() != null) {
// v2.0 - Utiliser les nouveaux noms de champs
this.uploadedBy = new MediaUploaderDTO(
media.getUploadedBy().getId().toString(),
media.getUploadedBy().getFirstName(),
media.getUploadedBy().getLastName(),
media.getUploadedBy().getProfileImageUrl()
);
}
}
/**
* DTO interne pour les informations de l'uploader.
*/
@Getter
public static class MediaUploaderDTO {
private final String id;
private final String firstName;
private final String lastName;
private final String profileImageUrl;
public MediaUploaderDTO(String id, String firstName, String lastName, String profileImageUrl) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.profileImageUrl = profileImageUrl;
}
}
}

View File

@@ -1,37 +0,0 @@
package com.lions.dev.dto.response.establishment;
import com.lions.dev.entity.establishment.EstablishmentRating;
import lombok.Getter;
import java.time.LocalDateTime;
/**
* DTO pour renvoyer les informations d'une note d'établissement.
*/
@Getter
public class EstablishmentRatingResponseDTO {
private String id;
private String establishmentId;
private String userId;
private Integer rating;
private String comment;
private LocalDateTime ratedAt;
private LocalDateTime updatedAt;
/**
* Constructeur qui transforme une entité EstablishmentRating en DTO.
*
* @param rating La note à convertir en DTO.
*/
public EstablishmentRatingResponseDTO(EstablishmentRating rating) {
this.id = rating.getId().toString();
this.establishmentId = rating.getEstablishment().getId().toString();
this.userId = rating.getUser().getId().toString();
this.rating = rating.getRating();
this.comment = rating.getComment();
this.ratedAt = rating.getRatedAt();
this.updatedAt = rating.getUpdatedAt();
}
}

View File

@@ -1,30 +0,0 @@
package com.lions.dev.dto.response.establishment;
import lombok.Getter;
import java.util.Map;
/**
* DTO pour renvoyer les statistiques de notation d'un établissement.
*/
@Getter
public class EstablishmentRatingStatsResponseDTO {
private Double averageRating; // Note moyenne (0.0 à 5.0)
private Integer totalRatings; // Nombre total de notes
private Map<Integer, Integer> distribution; // Distribution par étoile {5: 10, 4: 5, ...}
/**
* Constructeur pour créer les statistiques de notation.
*
* @param averageRating La note moyenne
* @param totalRatings Le nombre total de notes
* @param distribution La distribution des notes par étoile
*/
public EstablishmentRatingStatsResponseDTO(Double averageRating, Integer totalRatings, Map<Integer, Integer> distribution) {
this.averageRating = averageRating;
this.totalRatings = totalRatings;
this.distribution = distribution;
}
}

View File

@@ -1,154 +0,0 @@
package com.lions.dev.dto.response.establishment;
import com.lions.dev.entity.establishment.Establishment;
import com.lions.dev.entity.establishment.MediaType;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour renvoyer les informations d'un établissement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé pour structurer les données retournées dans les réponses
* après les opérations sur les établissements (création, récupération, mise à jour).
*/
@Getter
public class EstablishmentResponseDTO {
private String id;
private String name;
private String type;
private String address;
private String city;
private String postalCode;
private String description;
private String phoneNumber;
private String website;
private Double averageRating; // Note moyenne calculée
private Integer totalReviewsCount; // v2.0 - renommé depuis totalRatingsCount
private String priceRange;
private String verificationStatus; // v2.0 - PENDING, VERIFIED, REJECTED
private Double latitude;
private Double longitude;
private String managerId;
private String managerEmail;
private String managerFirstName; // v2.0
private String managerLastName; // v2.0
private String mainImageUrl; // v2.0 - URL de l'image principale (premier média avec displayOrder 0)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/** Nombre maximum de places dans l'établissement (optionnel). */
private Integer capacity;
/** Places restantes (capacity - participants des événements ouverts/à venir). Null si capacity non défini. */
private Integer remainingPlaces;
// Champs dépréciés (v1.0) - conservés pour compatibilité
/**
* @deprecated Utiliser {@link #averageRating} à la place.
*/
@Deprecated
private Double rating;
/**
* @deprecated Supprimé en v2.0 (utiliser manager.email à la place).
*/
@Deprecated
private String email;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_media à la place).
*/
@Deprecated
private String imageUrl;
/**
* @deprecated Supprimé en v2.0 (utiliser establishment_amenities à la place).
*/
@Deprecated
private String amenities;
/**
* @deprecated Supprimé en v2.0 (utiliser business_hours à la place).
*/
@Deprecated
private String openingHours;
/**
* @deprecated Utiliser {@link #totalReviewsCount} à la place.
*/
@Deprecated
private Integer totalRatingsCount;
/**
* Constructeur qui transforme une entité Establishment en DTO (v2.0).
*
* @param establishment L'établissement à convertir en DTO.
*/
public EstablishmentResponseDTO(Establishment establishment) {
this.id = establishment.getId().toString();
this.name = establishment.getName();
this.type = establishment.getType();
this.address = establishment.getAddress();
this.city = establishment.getCity();
this.postalCode = establishment.getPostalCode();
this.description = establishment.getDescription();
this.phoneNumber = establishment.getPhoneNumber();
this.website = establishment.getWebsite();
this.averageRating = establishment.getAverageRating();
this.totalReviewsCount = establishment.getTotalReviewsCount(); // v2.0
this.priceRange = establishment.getPriceRange();
this.verificationStatus = establishment.getVerificationStatus(); // v2.0
this.latitude = establishment.getLatitude();
this.longitude = establishment.getLongitude();
if (establishment.getManager() != null) {
this.managerId = establishment.getManager().getId().toString();
this.managerEmail = establishment.getManager().getEmail();
this.managerFirstName = establishment.getManager().getFirstName(); // v2.0
this.managerLastName = establishment.getManager().getLastName(); // v2.0
}
// Récupérer l'image principale (premier média photo avec displayOrder 0 ou le premier disponible)
if (establishment.getMedias() != null && !establishment.getMedias().isEmpty()) {
this.mainImageUrl = establishment.getMedias().stream()
.filter(media -> media.getMediaType() == MediaType.PHOTO)
.sorted((a, b) -> Integer.compare(
a.getDisplayOrder() != null ? a.getDisplayOrder() : Integer.MAX_VALUE,
b.getDisplayOrder() != null ? b.getDisplayOrder() : Integer.MAX_VALUE))
.map(media -> media.getMediaUrl())
.findFirst()
.orElse(null);
} else {
this.mainImageUrl = null;
}
this.createdAt = establishment.getCreatedAt();
this.updatedAt = establishment.getUpdatedAt();
this.capacity = establishment.getCapacity();
this.remainingPlaces = null; // Sera renseigné via le constructeur avec occupiedPlaces si besoin
// Compatibilité v1.0 - valeurs null pour les champs dépréciés
this.rating = null;
this.email = null;
this.imageUrl = null;
this.amenities = null;
this.openingHours = null;
this.totalRatingsCount = this.totalReviewsCount; // Alias pour compatibilité
}
/**
* Constructeur avec calcul des places restantes (capacity - participants des événements ouverts/à venir).
*/
public EstablishmentResponseDTO(Establishment establishment, Integer occupiedPlaces) {
this(establishment);
if (this.capacity != null && occupiedPlaces != null) {
this.remainingPlaces = Math.max(0, this.capacity - occupiedPlaces);
}
}
}

View File

@@ -1,22 +0,0 @@
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;
}

View File

@@ -1,16 +1,10 @@
package com.lions.dev.dto.response.events;
import com.lions.dev.entity.events.Events;
import com.lions.dev.repository.UsersRepository;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour renvoyer les informations d'un événement.
*
* Version 2.0 - Architecture refactorée avec nommage standardisé.
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
*
* Ce DTO est utilisé pour structurer les données retournées dans les réponses
* après les opérations sur les événements (création, récupération).
*/
@@ -22,95 +16,33 @@ public class EventCreateResponseDTO {
private String description; // Description de l'événement
private LocalDateTime startDate; // Date de début de l'événement
private LocalDateTime endDate; // Date de fin de l'événement
private String establishmentId; // v2.0 - ID de l'établissement où se déroule l'événement
private String establishmentName; // v2.0 - Nom de l'établissement
private String location; // Lieu de l'événement
private String category; // Catégorie de l'événement
private String link; // Lien vers plus d'informations
private String imageUrl; // URL d'une image pour l'événement
private String creatorId; // ID du créateur de l'événement
private String creatorEmail; // Email du créateur de l'événement
private String creatorFirstName; // v2.0 - Prénom du créateur de l'événement
private String creatorLastName; // v2.0 - Nom de famille du créateur de l'événement
private String status; // Statut de l'événement (OPEN, CLOSED, CANCELLED, COMPLETED)
private Boolean isPrivate; // v2.0 - Indique si l'événement est privé
private Boolean waitlistEnabled; // v2.0 - Indique si la liste d'attente est activée
private Integer maxParticipants; // Nombre maximum de participants autorisés
private Integer participationFee; // Frais de participation en centimes
private Integer participantsCount; // ✅ Nombre actuel de participants (event.getParticipants().size())
private Integer commentsCount; // ✅ Nombre de commentaires (event.getComments().size())
private Integer sharesCount; // ✅ Nombre de partages (event.getShares().size())
private Long reactionsCount; // ✅ Nombre de réactions (utilisateurs qui ont cet événement en favori)
private Boolean isFavorite; // ✅ Indique si l'utilisateur actuel a cet événement en favori (optionnel, dépend du contexte)
// Champ déprécié (v1.0) - conservé pour compatibilité
/**
* @deprecated Utiliser {@link #establishmentId} et {@link #establishmentName} à la place.
*/
@Deprecated
private String location;
private String creatorFirstName; // Prénom du créateur de l'événement
private String creatorLastName; // Nom de famille du création de l'événement
private String status; // Statut de l'événement
/**
* Constructeur qui transforme une entité Events en DTO (v2.0).
* Utilise UsersRepository pour calculer reactionsCount et isFavorite.
* Constructeur qui transforme une entité Events en DTO.
*
* @param event L'événement à convertir en DTO.
* @param usersRepository Le repository pour compter les réactions (peut être null).
* @param currentUserId L'ID de l'utilisateur actuel pour vérifier isFavorite (peut être null).
*/
public EventCreateResponseDTO(Events event, UsersRepository usersRepository, UUID currentUserId) {
public EventCreateResponseDTO(Events event) {
this.id = event.getId().toString();
this.title = event.getTitle();
this.description = event.getDescription();
this.startDate = event.getStartDate();
this.endDate = event.getEndDate();
this.location = event.getLocation();
this.category = event.getCategory();
this.link = event.getLink();
this.imageUrl = event.getImageUrl();
this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getPrenoms();
this.creatorLastName = event.getCreator().getNom();
this.status = event.getStatus();
this.isPrivate = event.getIsPrivate(); // v2.0
this.waitlistEnabled = event.getWaitlistEnabled(); // v2.0
this.maxParticipants = event.getMaxParticipants();
this.participationFee = event.getParticipationFee();
this.participantsCount = event.getParticipants() != null ? event.getParticipants().size() : 0;
this.commentsCount = event.getComments() != null ? event.getComments().size() : 0;
this.sharesCount = event.getShares() != null ? event.getShares().size() : 0;
// ✅ Calculer reactionsCount si usersRepository est fourni
if (usersRepository != null) {
this.reactionsCount = usersRepository.countUsersWithFavoriteEvent(event.getId());
} else {
this.reactionsCount = 0L;
}
// ✅ Vérifier isFavorite si currentUserId est fourni
if (currentUserId != null && usersRepository != null) {
this.isFavorite = usersRepository.hasUserFavoriteEvent(currentUserId, event.getId());
} else {
this.isFavorite = null;
}
// v2.0 - Informations sur l'établissement
if (event.getEstablishment() != null) {
this.establishmentId = event.getEstablishment().getId().toString();
this.establishmentName = event.getEstablishment().getName();
this.location = event.getLocation(); // Méthode qui retourne l'adresse de l'établissement
}
// v2.0 - Informations sur le créateur
if (event.getCreator() != null) {
this.creatorId = event.getCreator().getId().toString();
this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getFirstName(); // v2.0
this.creatorLastName = event.getCreator().getLastName(); // v2.0
}
}
/**
* Constructeur simplifié sans calcul de réactions (pour compatibilité).
*
* @param event L'événement à convertir en DTO.
*/
public EventCreateResponseDTO(Events event) {
this(event, null, null);
}
}

View File

@@ -27,7 +27,7 @@ public class EventReadManyByIdResponseDTO {
private String profileImageUrl; // URL de l'image de profil de l'utilisateur qui a criané l'événement
/**
* Constructeur qui transforme une entité Events en DTO de réponse (v2.0).
* Constructeur qui transforme une entité Events en DTO de réponse.
*
* @param event L'événement à convertir en DTO.
*/
@@ -37,16 +37,14 @@ public class EventReadManyByIdResponseDTO {
this.description = event.getDescription();
this.startDate = event.getStartDate();
this.endDate = event.getEndDate();
// v2.0 - Utiliser getLocation() qui retourne l'adresse de l'établissement
this.location = event.getLocation();
this.category = event.getCategory();
this.link = event.getLink();
this.imageUrl = event.getImageUrl();
this.status = event.getStatus();
this.creatorEmail = event.getCreator().getEmail();
// v2.0 - Utiliser les nouveaux noms de champs
this.creatorFirstName = event.getCreator().getFirstName();
this.creatorLastName = event.getCreator().getLastName();
this.creatorFirstName = event.getCreator().getPrenoms();
this.creatorLastName = event.getCreator().getNom();
this.profileImageUrl = event.getCreator().getProfileImageUrl();
}
}

View File

@@ -27,24 +27,8 @@ public class FriendshipCreateOneResponseDTO {
/**
* Constructeur pour mapper l'entité `Friendship` à ce DTO.
* Utilise les IDs fournis pour éviter LazyInitializationException sur user/friend.
*
* @param friendship L'entité `Friendship` à convertir en DTO.
* @param userId ID de l'utilisateur qui envoie la demande (déjà chargé).
* @param friendId ID de l'utilisateur qui reçoit la demande (déjà chargé).
*/
public FriendshipCreateOneResponseDTO(Friendship friendship, UUID userId, UUID friendId) {
this.id = friendship.getId();
this.userId = userId;
this.friendId = friendId;
this.status = friendship.getStatus();
this.createdAt = friendship.getCreatedAt();
this.updatedAt = friendship.getUpdatedAt();
}
/**
* Constructeur pour mapper l'entité `Friendship` à ce DTO (charge les associations lazy).
* Préférer {@link #FriendshipCreateOneResponseDTO(Friendship, UUID, UUID)} en fin de transaction.
*/
public FriendshipCreateOneResponseDTO(Friendship friendship) {
this.id = friendship.getId();

View File

@@ -36,12 +36,11 @@ public class FriendshipReadStatusResponseDTO {
public FriendshipReadStatusResponseDTO(Friendship friendship) {
this.friendshipId = friendship.getId();
this.userId = friendship.getUser().getId();
// v2.0 - Utiliser les nouveaux noms de champs
this.userNom = friendship.getUser().getLastName();
this.userPrenoms = friendship.getUser().getFirstName();
this.userNom = friendship.getUser().getNom();
this.userPrenoms = friendship.getUser().getPrenoms();
this.friendId = friendship.getFriend().getId();
this.friendNom = friendship.getFriend().getLastName();
this.friendPrenoms = friendship.getFriend().getFirstName();
this.friendNom = friendship.getFriend().getNom();
this.friendPrenoms = friendship.getFriend().getPrenoms();
this.status = friendship.getStatus();
this.createdAt = friendship.getCreatedAt();
}

View File

@@ -1,52 +0,0 @@
package com.lions.dev.dto.response.notifications;
import com.lions.dev.entity.notification.Notification;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO (Data Transfer Object) pour la réponse d'une notification.
*
* Cette classe sert de représentation simplifiée d'une notification
* pour la réponse de l'API.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class NotificationResponseDTO {
private UUID id;
private String title;
private String message;
private String type;
private boolean isRead;
private LocalDateTime timestamp;
private UUID userId;
private UUID eventId; // Optionnel
private String metadata; // Optionnel
/**
* Constructeur à partir d'une entité Notification.
*
* @param notification L'entité Notification
*/
public NotificationResponseDTO(Notification notification) {
if (notification != null) {
this.id = notification.getId();
this.title = notification.getTitle();
this.message = notification.getMessage();
this.type = notification.getType();
this.isRead = notification.isRead();
this.timestamp = notification.getCreatedAt();
this.userId = notification.getUser() != null ? notification.getUser().getId() : null;
this.eventId = notification.getEvent() != null ? notification.getEvent().getId() : null;
this.metadata = notification.getMetadata();
}
}
}

View File

@@ -1,79 +0,0 @@
package com.lions.dev.dto.response.promotion;
import com.lions.dev.entity.promotion.Promotion;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO pour la réponse d'une promotion.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PromotionResponseDTO {
private UUID id;
private UUID establishmentId;
private String establishmentName;
private String title;
private String description;
private String promoCode;
private String discountType;
private BigDecimal discountValue;
private LocalDateTime validFrom;
private LocalDateTime validUntil;
private Boolean isActive;
private boolean isValid;
private boolean isExpired;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur à partir d'une entité Promotion.
*
* @param promotion L'entité Promotion
*/
public PromotionResponseDTO(Promotion promotion) {
if (promotion != null) {
this.id = promotion.getId();
this.establishmentId = promotion.getEstablishment() != null ? promotion.getEstablishment().getId() : null;
this.establishmentName = promotion.getEstablishment() != null ? promotion.getEstablishment().getName() : null;
this.title = promotion.getTitle();
this.description = promotion.getDescription();
this.promoCode = promotion.getPromoCode();
this.discountType = promotion.getDiscountType();
this.discountValue = promotion.getDiscountValue();
this.validFrom = promotion.getValidFrom();
this.validUntil = promotion.getValidUntil();
this.isActive = promotion.getIsActive();
this.isValid = promotion.isValid();
this.isExpired = promotion.isExpired();
this.createdAt = promotion.getCreatedAt();
this.updatedAt = promotion.getUpdatedAt();
}
}
/**
* Formate la réduction pour l'affichage.
*
* @return La réduction formatée (ex: "20%", "10€", "1 article offert")
*/
public String getFormattedDiscount() {
if (discountValue == null || discountType == null) {
return "";
}
return switch (discountType.toUpperCase()) {
case "PERCENTAGE" -> discountValue.stripTrailingZeros().toPlainString() + "%";
case "FIXED_AMOUNT" -> discountValue.stripTrailingZeros().toPlainString() + "";
case "FREE_ITEM" -> discountValue.intValue() + " article(s) offert(s)";
default -> discountValue.toString();
};
}
}

View File

@@ -1,73 +0,0 @@
package com.lions.dev.dto.response.review;
import com.lions.dev.entity.establishment.Review;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
/**
* DTO pour la réponse d'un avis.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReviewResponseDTO {
private UUID id;
private UUID userId;
private String userFirstName;
private String userLastName;
private String userProfileImageUrl;
private UUID establishmentId;
private String establishmentName;
private Integer overallRating;
private String comment;
private Map<String, Integer> criteriaRatings;
private Boolean isVerifiedVisit;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur à partir d'une entité Review.
*
* @param review L'entité Review
*/
public ReviewResponseDTO(Review review) {
if (review != null) {
this.id = review.getId();
this.userId = review.getUser() != null ? review.getUser().getId() : null;
this.userFirstName = review.getUser() != null ? review.getUser().getFirstName() : null;
this.userLastName = review.getUser() != null ? review.getUser().getLastName() : null;
this.userProfileImageUrl = review.getUser() != null ? review.getUser().getProfileImageUrl() : null;
this.establishmentId = review.getEstablishment() != null ? review.getEstablishment().getId() : null;
this.establishmentName = review.getEstablishment() != null ? review.getEstablishment().getName() : null;
this.overallRating = review.getOverallRating();
this.comment = review.getComment();
this.criteriaRatings = review.getCriteriaRatings();
this.isVerifiedVisit = review.getIsVerifiedVisit();
this.createdAt = review.getCreatedAt();
this.updatedAt = review.getUpdatedAt();
}
}
/**
* Retourne le nom complet de l'auteur de l'avis.
*/
public String getUserFullName() {
StringBuilder sb = new StringBuilder();
if (userFirstName != null) {
sb.append(userFirstName);
}
if (userLastName != null) {
if (sb.length() > 0) sb.append(" ");
sb.append(userLastName);
}
return sb.toString();
}
}

View File

@@ -1,69 +0,0 @@
package com.lions.dev.dto.response.social;
import com.lions.dev.entity.social.PostComment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* DTO (Data Transfer Object) pour la réponse d'un commentaire de post social.
*
* Cette classe représente un commentaire avec les informations de l'auteur
* pour l'affichage dans l'interface utilisateur.
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PostCommentResponseDTO {
private UUID id;
private String content;
private UUID postId;
private UUID userId;
private String userFirstName;
private String userLastName;
private String userProfileImageUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/**
* Constructeur à partir d'une entité PostComment.
*
* @param comment L'entité PostComment
*/
public PostCommentResponseDTO(PostComment comment) {
if (comment != null) {
this.id = comment.getId();
this.content = comment.getContent();
this.postId = comment.getPost() != null ? comment.getPost().getId() : null;
this.userId = comment.getUser() != null ? comment.getUser().getId() : null;
this.userFirstName = comment.getUser() != null ? comment.getUser().getFirstName() : null;
this.userLastName = comment.getUser() != null ? comment.getUser().getLastName() : null;
this.userProfileImageUrl = comment.getUser() != null ? comment.getUser().getProfileImageUrl() : null;
this.createdAt = comment.getCreatedAt();
this.updatedAt = comment.getUpdatedAt();
}
}
/**
* Retourne le nom complet de l'auteur du commentaire.
*
* @return Le nom complet (prénom + nom)
*/
public String getUserFullName() {
StringBuilder sb = new StringBuilder();
if (userFirstName != null) {
sb.append(userFirstName);
}
if (userLastName != null) {
if (sb.length() > 0) sb.append(" ");
sb.append(userLastName);
}
return sb.toString();
}
}

Some files were not shown because too many files have changed in this diff Show More