Compare commits

...

42 Commits

Author SHA1 Message Date
dahoud
13d3097b3e Refactoring 2026-02-07 17:04:49 +00:00
dahoud
fc451f025e fix(security): Correction definitive de la verification JWT HS256
PROBLEME RESOLU:

- Les tokens JWT generes au login n'etaient pas verifies correctement

- SmallRye JWT ne pouvait pas charger la cle de verification

- Incompatibilite entre l'issuer du token et celui attendu

CORRECTIONS:

- Creation de jwt-secret.jwk au format JWK standard pour cles symetriques

- Configuration smallrye.jwt.verify.key.location vers le fichier JWK

- Alignement de l'issuer sur 'afterwork' dans .env.example

Ce commit sert de checkpoint stable pour la configuration JWT.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 02:44:35 +00:00
dahoud
e78423dd16 Refactoring 2026-02-05 18:21:13 +00:00
dahoud
4532a25427 Refactoring 2026-02-05 18:16:18 +00:00
dahoud
806efeb074 Refactoring 2026-02-05 18:09:30 +00:00
dahoud
2a794523b6 Refactoring - Bonne version améliorée 2026-02-05 16:30:20 +00:00
dahoud
dd4dbe111e Refactoring - Bonne version améliorée 2026-02-05 14:14:45 +00:00
dahoud
a515963a4a Refactoring 2026-02-04 12:46:56 +00:00
dahoud
c31c6174cc Refactoring 2026-02-04 01:06:17 +00:00
dahoud
40de25315c Refactoring 2026-02-02 19:22:36 +00:00
dahoud
950041719e Refactoring 2026-02-02 19:14:42 +00:00
dahoud
6e89295e6b Refactoring 2026-02-02 19:07:36 +00:00
dahoud
7021b7a7ce Refactoring 2026-02-02 01:37:11 +00:00
dahoud
bcbae7c599 Refactoring 2026-02-02 00:59:52 +00:00
dahoud
8f5267d895 Refactoring 2026-02-02 00:42:02 +00:00
dahoud
675e0925b8 Refactoring 2026-01-31 21:50:43 +00:00
dahoud
0240442671 fix(build): switch from uber-jar to fast-jar for Docker compatibility
- Change quarkus.package.type from uber-jar to fast-jar
- Add EventShare entity and migration for share tracking
- Add establishment capacity field
- Improve event and establishment services
- Add comprehensive tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 20:27:27 +00:00
dahoud
9dc9ca591c Refactoring 2026-01-31 16:54:46 +00:00
dahoud
ce89face73 feat: v2.0 – réorg docker/scripts, prod, résas, abonnements Wave, Flyway base vierge 2026-01-29 00:44:40 +00:00
dahoud
9d5e388efa Refactoring 2026-01-24 09:33:59 +00:00
dahoud
c5a65bab5b Refactoring 2026-01-21 21:46:21 +00:00
dahoud
cb8b9da12e Refactoring 2026-01-21 21:37:48 +00:00
dahoud
8cb67f1762 Refactoring 2026-01-21 19:16:24 +00:00
dahoud
b9fc1ee05a fix: ajouter application-production.properties et corriger config WebSockets Next 2026-01-21 18:18:20 +00:00
dahoud
93c63fd600 feat: migration complète vers WebSockets Next + Kafka pour temps réel
- Migration de Jakarta WebSocket vers Quarkus WebSockets Next
- Implémentation de l'architecture Kafka pour événements temps réel
- Ajout des DTOs d'événements (NotificationEvent, ChatMessageEvent, ReactionEvent, PresenceEvent)
- Création des bridges Kafka → WebSocket (NotificationKafkaBridge, ChatKafkaBridge, ReactionKafkaBridge)
- Mise à jour des services pour publier dans Kafka au lieu d'appeler directement WebSocket
- Suppression des classes obsolètes (ChatWebSocket, NotificationWebSocket)
- Correction de l'injection des paramètres path dans WebSockets Next (utilisation de connection.pathParam)
- Ajout des migrations DB pour bookings, promotions, business hours, amenities, reviews
- Mise à jour de la configuration application.properties pour Kafka et WebSockets Next
- Mise à jour .gitignore pour ignorer les fichiers de logs
2026-01-21 13:46:16 +00:00
dahoud
7dd0969799 fix(backend): Correction du système de réactions (favoris) pour les événements 2026-01-19 22:44:14 +00:00
dahoud
a5fd9538fe chore: Mise à jour de la configuration Kubernetes 2026-01-13 21:09:45 +00:00
dahoud
7309fcc72d config: Configuration des environnements et migrations DB - Séparation des configurations dev et prod - Configuration H2 pour développement - Configuration PostgreSQL pour production - Migration SQL pour les nouveaux champs d'événements (V2__Add_Event_Additional_Fields.sql) - Configuration CORS et root-path pour production 2026-01-13 20:45:28 +00:00
dahoud
c26098b0d4 fix: Corrections WebSocket et upload de fichiers - FileUploadResource: support des métadonnées (type, fileName, contentType, fileSize, userId) - NotificationWebSocket: correction de l'erreur JTA transaction avec CompletableFuture.runAsync() - PresenceService: ajout de @ActivateRequestContext pour le contexte de requête 2026-01-13 20:45:25 +00:00
dahoud
bfb174bcf8 fix: Correction de la comparaison des utilisateurs dans Conversation - Utilisation de getId().equals() au lieu de equals() - Correction dans updateLastMessage, markAllAsReadForUser, etc. 2026-01-13 20:45:21 +00:00
dahoud
0443bd251f feat: Extension des événements avec 10 nouveaux champs - Ajout de maxParticipants, tags, organizer, participationFee - Ajout de privacyRules, transportInfo, accommodationInfo - Ajout de accessibilityInfo, parkingInfo, securityProtocol - Mise à jour des DTOs et services - Vérification des permissions pour le CRUD (créateur uniquement) - Ajout de creatorId dans EventCreateResponseDTO 2026-01-13 20:45:18 +00:00
dahoud
56d0aad6a6 feat: Système complet de gestion des établissements (backend) - Entités JPA pour Establishment, EstablishmentMedia, EstablishmentRating - DTOs pour création, mise à jour et réponses - Repositories Panache pour accès aux données - Services avec logique métier et validation - Resources REST avec tous les endpoints CRUD - Gestion des médias (photos/vidéos) - Système de notation avec statistiques 2026-01-13 20:45:13 +00:00
dahoud
c0b1863467 fix(ingress): Retrait des snippets désactivés et utilisation annotations standards 2026-01-10 16:22:34 +00:00
dahoud
9cf41a3b7e fix(config): Correction propriétés multipart et ingress aligné sur btpxpress
- Remplacement des propriétés obsolètes multipart par quarkus.http.limits.max-body-size
- Mise à jour de l'ingress avec le bon nom de service et annotations lionsctl
- Utilisation du certificat partagé api-lions-dev-tls
- Ajout du support WebSocket dans l'ingress
2026-01-10 16:08:08 +00:00
dahoud
9499ecb66a fix(config): Nettoyage propriétés build-time dans application-prod
- Suppression des propriétés build-time qui causaient des warnings
- Mise à jour des credentials par défaut: lionsuser/LionsUser2025!
- Mise à jour database name: mic-after-work-server-impl-quarkus-main
- Les propriétés build-time restent dans application.properties
2026-01-10 15:46:28 +00:00
dahoud
f63cc63d9d fix(config): Utilisation de PostgreSQL comme datasource par défaut
- Changement datasource par défaut de H2 vers PostgreSQL
- Mise à jour credentials: lionsuser / LionsUser2025!
- Database: mic-after-work-server-impl-quarkus-main
- Résout l'erreur 'Driver does not support the provided URL'
2026-01-10 15:04:15 +00:00
dahoud
d659416627 fix(docker): Autoriser target/ et *.jar dans le contexte Docker
Le Dockerfile copie le JAR depuis target/, donc ces fichiers
doivent être inclus dans le contexte Docker build.
2026-01-10 13:35:44 +00:00
dahoud
0dafe9ce7f fix(build): Configuration uber-jar par défaut dans pom.xml
Force la génération d'un uber-jar (*-runner.jar) à chaque build Maven.
Compatible avec le Dockerfile qui attend target/*-runner.jar.
2026-01-10 13:32:23 +00:00
dahoud
730581a46b fix(build): Ajout datasource H2 par défaut pour le build
Permet le build Quarkus sans variables d'environnement.
La datasource H2 sera remplacée au runtime par PostgreSQL via env vars.
2026-01-10 13:06:44 +00:00
dahoud
a09cdfb67d fix(build): Ajout valeurs par défaut datasource pour build production
Permet le build avec -Dquarkus.profile=production sans variables d'environnement.
Les vraies valeurs seront fournies par Kubernetes au runtime.

- application.properties: Ajout valeurs par défaut pour DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD
- deploy.ps1: Ajout -Dquarkus.package.type=uber-jar pour générer runner.jar
2026-01-10 13:01:52 +00:00
dahoud
044c18fe09 fix(config): Alignement configuration DB avec unionflow et btpxpress
Corrections pour assurer la cohérence avec les autres projets en production :

## Changements

### DB_HOST: postgres → postgresql
- kubernetes/afterwork-configmap.yaml
- src/main/resources/application-prod.properties (défaut)
- Dockerfile.prod (ENV)

### DB_PASSWORD: Pattern cohérent
- kubernetes/afterwork-secrets.yaml
- Nouveau mot de passe: AfterWork2025!
- Suit le pattern observé dans unionflow (UnionFlow2025!) et btpxpress

## Analyse des Projets Existants

### BTPXpress
- Host: postgresql
- User: btpxpress
- Password: btpxpress_secure_2024

### UnionFlow
- Host: postgresql (implicite)
- User: unionflow
- Password: UnionFlow2025!

### AfterWork (Corrigé)
- Host: postgresql 
- User: afterwork 
- Password: AfterWork2025! 

## Documentation

- DATABASE_CONFIG.md : Guide complet de configuration DB
  - Paramètres de connexion
  - Commandes de vérification
  - Troubleshooting
  - Checklist de déploiement

## Impact

 Configuration cohérente avec les autres projets
 Évite les erreurs de connexion au déploiement
 Pattern de sécurité uniforme
 Documentation complète pour maintenance
2026-01-10 11:25:49 +00:00
dahoud
093d04c224 feat(backend): Ajout complet des fonctionnalités Chat, Social, Story et Notifications
Implémentation complète de toutes les fonctionnalités backend :

## Nouvelles Fonctionnalités

### Chat (Messagerie Instantanée)
- Entities : Conversation, Message
- DTOs : ConversationResponseDTO, MessageResponseDTO, SendMessageRequestDTO
- Resources : MessageResource (endpoints REST)
- Services : MessageService (logique métier)
- Repositories : ConversationRepository, MessageRepository
- WebSocket : ChatWebSocket (temps réel)

### Social (Publications Sociales)
- Entities : SocialPost, SocialComment, SocialLike
- DTOs : SocialPostResponseDTO, CreateSocialPostRequestDTO
- Resources : SocialPostResource
- Services : SocialPostService
- Repositories : SocialPostRepository

### Story (Stories temporaires)
- Entities : Story, StoryView
- DTOs : StoryResponseDTO, CreateStoryRequestDTO
- Resources : StoryResource
- Services : StoryService
- Repositories : StoryRepository

### Notifications (Temps Réel)
- Entities : Notification
- DTOs : NotificationResponseDTO
- Resources : NotificationResource
- Services : NotificationService, PresenceService
- Repositories : NotificationRepository
- WebSocket : NotificationWebSocket (temps réel)

## Améliorations

### Users & Friendship
- Mise à jour UserResponseDTO avec nouveaux champs
- Amélioration FriendshipResource avec séparation demandes envoyées/reçues
- FriendSuggestionResponseDTO pour suggestions d'amis
- Optimisations dans UsersService et FriendshipService

### Events
- Améliorations EventsResource et EventService
- Optimisations EventsRepository

### Configuration
- Mise à jour application.properties
- Configuration docker-compose.yml
- Dockerfile pour développement

## Fichiers Modifiés
- .dockerignore, .gitignore
- README.md
- docker-compose.yml
- Configuration Maven wrapper
2026-01-10 10:39:58 +00:00
256 changed files with 34410 additions and 1669 deletions

View File

@@ -1,5 +1,48 @@
* # Build artifacts
!target/*-runner # Note: target/ et *.jar sont nécessaires pour le Docker build
!target/*-runner.jar # car on copie le JAR runner depuis target/
!target/lib/* *.war
!target/quarkus-app/* *.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/

129
.env.example Normal file
View File

@@ -0,0 +1,129 @@
# ============================================
# 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,43 +1,105 @@
#Maven # ====================
# Maven
# ====================
target/ target/
pom.xml.tag pom.xml.tag
pom.xml.releaseBackup pom.xml.releaseBackup
pom.xml.versionsBackup pom.xml.versionsBackup
release.properties release.properties
.flattened-pom.xml .flattened-pom.xml
dependency-reduced-pom.xml
# Eclipse # ====================
# IDE - Eclipse
# ====================
.project .project
.classpath .classpath
.settings/ .settings/
bin/ bin/
# IntelliJ # ====================
.idea # IDE - IntelliJ IDEA
# ====================
.idea/
*.ipr *.ipr
*.iml *.iml
*.iws *.iws
out/
# NetBeans # ====================
# IDE - NetBeans
# ====================
nb-configuration.xml nb-configuration.xml
# Visual Studio Code # ====================
.vscode # IDE - Visual Studio Code
# ====================
.vscode/
.factorypath .factorypath
# OSX # ====================
# OS - macOS
# ====================
.DS_Store .DS_Store
._*
# Vim # ====================
# OS - Windows
# ====================
Thumbs.db
Desktop.ini
ehthumbs.db
# ====================
# Vim / Editors
# ====================
*.swp *.swp
*.swo *.swo
*~
# patch # ====================
# Patch files
# ====================
*.orig *.orig
*.rej *.rej
# Local environment # ====================
# Environment & Secrets
# ====================
.env .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
# Plugin directory # ====================
# Quarkus
# ====================
/.quarkus/cli/plugins/ /.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

4
.mvn/jvm.config Normal file
View File

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

View File

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

View File

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

View File

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

305
DATABASE_CONFIG.md Normal file
View File

@@ -0,0 +1,305 @@
# 🗄️ 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

@@ -2,7 +2,7 @@
## 📋 Vue d'Ensemble ## 📋 Vue d'Ensemble
Ce guide décrit le processus de déploiement de l'API AfterWork sur le VPS via `lionesctl pipeline`. 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` **URL de l'API** : `https://api.lions.dev/afterwork`
@@ -14,7 +14,7 @@ Ce guide décrit le processus de déploiement de l'API AfterWork sur le VPS via
- Java 17 (JDK) - Java 17 (JDK)
- Maven 3.9+ - Maven 3.9+
- Docker 20.10+ - Docker 20.10+
- `lionesctl` CLI installé et configuré - `lionsctl` CLI installé et configuré
### Environnement Serveur ### Environnement Serveur
- PostgreSQL 15+ - PostgreSQL 15+
@@ -38,9 +38,9 @@ DB_USERNAME: afterwork # Utilisateur de la base de données
DB_PASSWORD: <secret> # Mot de passe (à définir dans le secret) DB_PASSWORD: <secret> # Mot de passe (à définir dans le secret)
``` ```
### 2. Dockerfile.prod ### 2. docker/Dockerfile.prod
Le fichier `Dockerfile.prod` utilise une approche multi-stage : Le fichier `docker/Dockerfile.prod` utilise une approche multi-stage :
- **Stage 1** : Build avec Maven dans une image UBI8 OpenJDK 17 - **Stage 1** : Build avec Maven dans une image UBI8 OpenJDK 17
- **Stage 2** : Runtime optimisé avec l'uber-jar compilé - **Stage 2** : Runtime optimisé avec l'uber-jar compilé
@@ -59,8 +59,8 @@ Configuration production avec :
### Build Local (Test) ### Build Local (Test)
```bash ```bash
# Build de l'image # Build de l'image (Dockerfiles dans docker/)
docker build -f Dockerfile.prod -t afterwork-api:latest . docker build -f docker/Dockerfile.prod -t afterwork-api:latest .
# Test local # Test local
docker run -p 8080:8080 \ docker run -p 8080:8080 \
@@ -86,31 +86,66 @@ docker push registry.lions.dev/afterwork-api:latest
--- ---
## 🚢 Déploiement avec lionesctl ## 🚢 Déploiement avec lionsctl pipeline
### Commande de Déploiement 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 ```bash
# Déploiement via lionesctl pipeline # Déploiement en dev (clone + build + image + déploiement K8s)
lionesctl pipeline deploy \ lionsctl pipeline \
--app afterwork-api \ -u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
--image registry.lions.dev/afterwork-api:1.0.0 \ -b develop \
--namespace applications \ -j 17 \
--port 8080 \ -e dev \
--replicas 2 -c k1 \
-m <email>
# Ou avec le fichier de configuration # Déploiement en production sur le cluster k2
lionesctl pipeline deploy -f kubernetes/afterwork-deployment.yaml 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
``` ```
### Vérification du Déploiement **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 ```bash
# Status du déploiement # Pods et statut (nom d'app dérivé du repo, ex. mic-after-work-server-impl-quarkus-main)
lionesctl pipeline status --app afterwork-api kubectl get pods -n applications -l app=mic-after-work-server-impl-quarkus-main
# Logs en temps réel # Logs en temps réel
lionesctl pipeline logs --app afterwork-api --follow kubectl logs -n applications -l app=mic-after-work-server-impl-quarkus-main -f
# Health check # Health check
curl https://api.lions.dev/afterwork/q/health/ready curl https://api.lions.dev/afterwork/q/health/ready
@@ -284,8 +319,8 @@ ls target/*-runner.jar
### Étape 2 : Build Docker ### Étape 2 : Build Docker
```bash ```bash
# Build l'image de production # Build l'image de production (Dockerfiles dans docker/)
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 . docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
# Test local (optionnel) # Test local (optionnel)
docker run --rm -p 8080:8080 \ docker run --rm -p 8080:8080 \
@@ -325,8 +360,8 @@ kubectl apply -f kubernetes/afterwork-deployment.yaml
kubectl apply -f kubernetes/afterwork-service.yaml kubectl apply -f kubernetes/afterwork-service.yaml
kubectl apply -f kubernetes/afterwork-ingress.yaml kubectl apply -f kubernetes/afterwork-ingress.yaml
# Ou via lionesctl pipeline # Ou via lionsctl pipeline (clone + build + déploiement)
lionesctl pipeline deploy -f kubernetes/ 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 ### Étape 5 : Vérification
@@ -361,7 +396,7 @@ curl https://api.lions.dev/afterwork/api/users/test
```bash ```bash
# 1. Build nouvelle version # 1. Build nouvelle version
mvn clean package -DskipTests mvn clean package -DskipTests
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.1 . docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.1 .
docker push registry.lions.dev/afterwork-api:1.0.1 docker push registry.lions.dev/afterwork-api:1.0.1
# 2. Mise à jour du déploiement # 2. Mise à jour du déploiement

View File

@@ -1,281 +0,0 @@
# ✅ Statut du Déploiement AfterWork API
**Date** : 2026-01-10
**Statut** : ✅ Prêt pour le déploiement
---
## 📋 Résumé de la Préparation
### ✅ Backend (Quarkus)
| Élément | Statut | Description |
|---------|--------|-------------|
| **Build Maven** | ✅ Validé | Build réussi avec uber-jar (73M) |
| **Tests** | ✅ Configuré | Non-bloquants (`testFailureIgnore=true`) |
| **Dockerfile.prod** | ✅ Créé | Multi-stage build avec UBI8 OpenJDK 17 |
| **.dockerignore** | ✅ Créé | Optimisation du contexte Docker |
| **application-prod.properties** | ✅ Créé | Configuration production avec context path `/afterwork` |
| **Kubernetes Manifests** | ✅ Créés | Deployment, Service, Ingress, ConfigMap, Secrets |
| **Scripts de déploiement** | ✅ Créés | `deploy.ps1` et documentation complète |
### ✅ Frontend (Flutter)
| Élément | Statut | Description |
|---------|--------|-------------|
| **env_config.dart** | ✅ Configuré | Support `--dart-define` pour API_BASE_URL |
| **build-prod.ps1** | ✅ Créé | Build APK/AAB avec `https://api.lions.dev/afterwork` |
| **Configuration API** | ✅ Prête | Pointe vers `https://api.lions.dev/afterwork` |
---
## 🔧 Fichiers Créés/Modifiés
### Backend
```
mic-after-work-server-impl-quarkus-main/
├── Dockerfile.prod ✅ NOUVEAU
├── .dockerignore ✅ NOUVEAU
├── pom.xml ✅ MODIFIÉ (tests non-bloquants)
├── deploy.ps1 ✅ NOUVEAU
├── DEPLOYMENT.md ✅ NOUVEAU
├── QUICK_DEPLOY.md ✅ NOUVEAU
├── DEPLOYMENT_STATUS.md ✅ NOUVEAU (ce fichier)
├── src/main/resources/
│ └── application-prod.properties ✅ NOUVEAU
└── kubernetes/
├── afterwork-configmap.yaml ✅ NOUVEAU
├── afterwork-secrets.yaml ✅ NOUVEAU (⚠️ MODIFIER MOT DE PASSE)
├── afterwork-deployment.yaml ✅ NOUVEAU
├── afterwork-service.yaml ✅ NOUVEAU
└── afterwork-ingress.yaml ✅ NOUVEAU
```
### Frontend
```
afterwork/
├── lib/core/constants/env_config.dart ✅ EXISTE (configuré)
└── build-prod.ps1 ✅ NOUVEAU
```
---
## 🚀 Prochaines Étapes pour le Déploiement
### 1⃣ Modifier le Secret de Base de Données
```bash
# Éditer le fichier
notepad C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main\kubernetes\afterwork-secrets.yaml
# Changer cette ligne:
DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION"
# Par le vrai mot de passe (encodé en base64 ou en clair avec stringData)
```
### 2⃣ Déployer via PowerShell Script (Recommandé)
```powershell
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Déploiement complet
.\deploy.ps1 -Action all -Version 1.0.0
# Ou étape par étape
.\deploy.ps1 -Action build # Build Maven + Docker
.\deploy.ps1 -Action push # Push vers registry
.\deploy.ps1 -Action deploy # Déploiement K8s
```
### 3⃣ Déployer via lionesctl (Alternative)
```bash
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Build local
mvn clean package -DskipTests -Dquarkus.package.type=uber-jar
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
docker push registry.lions.dev/afterwork-api:1.0.0
# Déploiement
lionesctl pipeline deploy -f kubernetes/
```
### 4⃣ Vérifier le Déploiement
```bash
# Pods
kubectl get pods -n applications -l app=afterwork-api
# Logs
kubectl logs -n applications -l app=afterwork-api -f
# Health check
curl https://api.lions.dev/afterwork/q/health/ready
curl https://api.lions.dev/afterwork/q/health/live
# Statut complet
.\deploy.ps1 -Action status
```
### 5⃣ Builder l'Application Flutter
```powershell
cd C:\Users\dadyo\PersonalProjects\lions-workspace\afterwork
# Build APK production
.\build-prod.ps1 -Target apk
# Ou AAB pour Play Store
.\build-prod.ps1 -Target appbundle
# Les artefacts seront dans:
# build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
```
---
## 📊 Tests de Build Effectués
### Build Maven (Validé ✅)
```
[INFO] BUILD SUCCESS
[INFO] Total time: 59.644 s
[INFO] Finished at: 2026-01-10T00:10:21Z
Artefact créé:
✅ target/mic-after-work-server-impl-quarkus-main-1.0.0-SNAPSHOT-runner.jar (73M)
```
**Notes:**
- Les tests sont skippés comme demandé
- Quelques warnings sur des configurations non reconnues (micrometer, health checks)
- Ces extensions sont probablement manquantes dans le pom.xml
- Cela n'empêche pas le déploiement
- Les health checks Quarkus fonctionneront avec les chemins par défaut
---
## ⚠️ Avertissements et Prérequis
### Prérequis pour le Déploiement
- [ ] PostgreSQL installé sur le cluster K8s
- [ ] Base de données `afterwork_db` créée
- [ ] Utilisateur `afterwork` avec droits appropriés
- [ ] Mot de passe DB configuré dans `kubernetes/afterwork-secrets.yaml`
- [ ] Docker installé et fonctionnel
- [ ] Accès au registry `registry.lions.dev`
- [ ] kubectl configuré avec accès au cluster
- [ ] Ingress Controller (nginx) installé
- [ ] Cert-Manager installé pour les certificats SSL
### Warnings Maven (Non-bloquants)
Les warnings suivants apparaissent lors du build mais n'empêchent pas le fonctionnement :
```
[WARNING] Unrecognized configuration key "quarkus.micrometer.*"
[WARNING] Unrecognized configuration key "quarkus.smallrye-health.*"
[WARNING] Unrecognized configuration key "quarkus.http.body.multipart.*"
```
**Solutions (Optionnel):**
Pour éliminer ces warnings, ajouter dans `pom.xml`:
```xml
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
```
Mais ce n'est pas nécessaire pour le déploiement initial.
---
## 🎯 Configuration des URLs
### Backend (Production)
- **API Base URL** : `https://api.lions.dev/afterwork`
- **Health Ready** : `https://api.lions.dev/afterwork/q/health/ready`
- **Health Live** : `https://api.lions.dev/afterwork/q/health/live`
- **Métriques** : `https://api.lions.dev/afterwork/q/metrics`
### WebSocket (Production)
- **Notifications** : `wss://api.lions.dev/afterwork/ws/notifications/{userId}`
- **Chat** : `wss://api.lions.dev/afterwork/ws/chat/{userId}`
### Frontend
- Configuré pour pointer vers `https://api.lions.dev/afterwork`
- Build production via `.\build-prod.ps1`
- Variables d'environnement injectées via `--dart-define`
---
## 📚 Documentation Disponible
1. **DEPLOYMENT.md** - Guide complet de déploiement (~566 lignes)
- Prérequis détaillés
- Structure Kubernetes complète
- Troubleshooting
- Monitoring et sécurité
2. **QUICK_DEPLOY.md** - Guide de déploiement rapide
- Commandes copier-coller
- 3 options de déploiement
- Checklist pré-déploiement
- Troubleshooting rapide
3. **deploy.ps1** - Script PowerShell automatisé
- Actions: build, push, deploy, all, rollback, status
- Validation et vérification automatique
- Gestion des erreurs
4. **DEPLOYMENT_STATUS.md** - Ce fichier
- Résumé de la préparation
- Statut actuel
- Prochaines étapes
---
## 🎉 Résumé
### ✅ Tous les fichiers nécessaires ont été créés
### ✅ Le build Maven fonctionne correctement
### ✅ L'uber-jar est généré avec succès (73M)
### ✅ Les tests sont configurés pour ne pas bloquer
### ✅ La documentation complète est disponible
### ✅ Le frontend est configuré pour production
## 🚀 L'API AfterWork est prête à être déployée !
---
**Commande recommandée pour déployer:**
```powershell
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# 1. Modifier le mot de passe DB dans kubernetes/afterwork-secrets.yaml
# 2. Lancer le déploiement
.\deploy.ps1 -Action all -Version 1.0.0
# 3. Vérifier
.\deploy.ps1 -Action status
curl https://api.lions.dev/afterwork/q/health/ready
```
---
**Pour toute question ou problème, consulter:**
- DEPLOYMENT.md (guide complet)
- QUICK_DEPLOY.md (guide rapide)
- Logs: `kubectl logs -n applications -l app=afterwork-api -f`

View File

@@ -8,15 +8,15 @@
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Déploiement complet (build + push + deploy) # Déploiement complet (build + push + deploy)
.\deploy.ps1 -Action all -Version 1.0.0 .\scripts\deploy.ps1 -Action all -Version 1.0.0
# Ou étape par étape # Ou étape par étape
.\deploy.ps1 -Action build # Build Maven + Docker .\scripts\deploy.ps1 -Action build # Build Maven + Docker
.\deploy.ps1 -Action push # Push vers registry .\scripts\deploy.ps1 -Action push # Push vers registry
.\deploy.ps1 -Action deploy # Déploiement K8s .\scripts\deploy.ps1 -Action deploy # Déploiement K8s
# Vérifier le statut # Vérifier le statut
.\deploy.ps1 -Action status .\scripts\deploy.ps1 -Action status
``` ```
### Option 2 : Déploiement Manuel ### Option 2 : Déploiement Manuel
@@ -28,7 +28,7 @@ cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
mvn clean package -DskipTests mvn clean package -DskipTests
# 2. Build Docker # 2. Build Docker
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 -t registry.lions.dev/afterwork-api:latest . docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 -t registry.lions.dev/afterwork-api:latest .
# 3. Push vers Registry # 3. Push vers Registry
docker login registry.lions.dev docker login registry.lions.dev
@@ -48,18 +48,15 @@ kubectl get pods -n applications -l app=afterwork-api
kubectl logs -n applications -l app=afterwork-api -f kubectl logs -n applications -l app=afterwork-api -f
``` ```
### Option 3 : Déploiement via lionesctl ### Option 3 : Déploiement via lionsctl pipeline
```bash ```bash
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
# Build local # Le pipeline clone le repo, build Maven, construit limage Docker et déploie sur K8s. Remplacer <org> et <email>.
mvn clean package -DskipTests
docker build -f Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
docker push registry.lions.dev/afterwork-api:1.0.0
# Déploiement # Déploiement
lionesctl pipeline deploy -f kubernetes/ 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>
``` ```
--- ---

View File

@@ -1,4 +1,4 @@
# mic-after-work # mic-after-work-server-impl-quarkus-main
This project uses Quarkus, the Supersonic Subatomic Java Framework. This project uses Quarkus, the Supersonic Subatomic Java Framework.
@@ -49,15 +49,52 @@ Or, if you don't have GraalVM installed, you can run the native executable build
./mvnw package -Dnative -Dquarkus.native.container-build=true ./mvnw package -Dnative -Dquarkus.native.container-build=true
``` ```
You can then execute your native executable with: `./target/mic-after-work-1.0.0-SNAPSHOT-runner` You can then execute your native executable with: `./target/mic-after-work-server-impl-quarkus-main-1.0.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/maven-tooling>. 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 ## Related Guides
- 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 - 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
- RESTEasy Classic ([guide](https://quarkus.io/guides/resteasy)): REST endpoint framework implementing Jakarta REST and more - RESTEasy Classic ([guide](https://quarkus.io/guides/resteasy)): REST endpoint framework implementing Jakarta REST and more
- JDBC Driver - Oracle ([guide](https://quarkus.io/guides/datasource)): Connect to the Oracle database via JDBC - 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.).
## Provided Code ## Provided Code
@@ -67,7 +104,6 @@ Create your first JPA entity
[Related guide section...](https://quarkus.io/guides/hibernate-orm) [Related guide section...](https://quarkus.io/guides/hibernate-orm)
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
### RESTEasy JAX-RS ### RESTEasy JAX-RS

119
REALTIME_DEV.md Normal file
View File

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

@@ -0,0 +1,930 @@
# 💻 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)

33
SECURITY.md Normal file
View File

@@ -0,0 +1,33 @@
# 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).

View File

@@ -1,412 +0,0 @@
# 🎉 Session de Travail Complétée - AfterWork
**Date** : 2026-01-10
**Projet** : AfterWork (Backend Quarkus + Frontend Flutter)
---
## 📋 Travail Effectué
Cette session a couvert deux grandes phases de travail :
### Phase 1 : Corrections et Implémentation des TODOs ✅
#### 1.1 Correction Critique - Race Condition Chat
**Problème** : Les icônes de statut des messages (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas.
**Cause** : Les confirmations WebSocket de délivrance arrivaient AVANT que les messages ne soient ajoutés à la liste locale (race condition entre HTTP response et WebSocket event).
**Solution** : Implémentation du pattern **Optimistic UI** dans `chat_bloc.dart`
- Création d'un message temporaire avec ID temporaire immédiatement
- Ajout à la liste AVANT la requête HTTP
- Remplacement du message temporaire par le message serveur à la réponse
**Fichiers modifiés:**
- `lib/presentation/state_management/chat_bloc.dart`
**Résultat** : ✅ Les statuts de message fonctionnent maintenant correctement
---
#### 1.2 Implémentation des TODOs (13/21)
| Fichier | TODOs Implémentés | Description |
|---------|-------------------|-------------|
| **social_header_widget.dart** | 3 | Copier lien, partage natif, signalement de post |
| **share_post_dialog.dart** | 2 | Sélection d'amis, partage externe |
| **media_upload_service.dart** | 3 | Parsing JSON, suppression média, génération miniature |
| **edit_post_dialog.dart** | 1 | Documentation chargement média |
| **create_post_dialog.dart** | 1 | Extraction URL depuis uploads |
| **conversations_screen.dart** | 2 | Navigation notifications, recherche conversations |
**Détails des implémentations:**
1. **social_header_widget.dart**
- ✅ Copier le lien du post dans le presse-papiers
- ✅ Partage natif via Share.share()
- ✅ Dialogue de signalement avec 5 raisons
2. **share_post_dialog.dart**
- ✅ Interface de sélection d'amis avec checkboxes
- ✅ Partage externe via Share API
3. **media_upload_service.dart**
- ✅ Parsing JSON de la réponse backend
- ✅ Méthode deleteMedia() pour supprimer les médias
- ✅ Génération de miniature vidéo avec video_thumbnail
4. **edit_post_dialog.dart**
- ✅ Documentation sur le chargement des médias existants
5. **create_post_dialog.dart**
- ✅ Extraction automatique des URLs depuis les médias uploadés
6. **conversations_screen.dart**
- ✅ Navigation vers écran de notifications depuis conversations
- ✅ ConversationSearchDelegate pour rechercher conversations par nom ou message
**Documentation créée:**
- `TODOS_IMPLEMENTED.md` (documentation complète de tous les TODOs)
---
### Phase 2 : Préparation du Déploiement Production ✅
#### 2.1 Infrastructure Backend
**Fichiers créés:**
1. **Dockerfile.prod** (Multi-stage build)
```dockerfile
- Stage 1: Build avec Maven + UBI8 OpenJDK 17
- Stage 2: Runtime optimisé avec uber-jar
- Healthcheck intégré
- User non-root (185) pour sécurité
```
2. **.dockerignore**
```
- Exclusion target/, tests, IDE, docs
- Optimisation du contexte Docker
```
3. **application-prod.properties**
```properties
- Context path: /afterwork
- CORS: https://afterwork.lions.dev
- Health checks: /q/health/ready, /q/health/live
- Compression HTTP activée
```
4. **pom.xml** (Modifié)
```xml
- testFailureIgnore: true
- skipTests: ${skipTests}
- Tests non-bloquants comme demandé
```
**Manifests Kubernetes créés:**
1. **afterwork-configmap.yaml**
- Variables non-sensibles : DB_HOST, DB_PORT, DB_NAME, etc.
2. **afterwork-secrets.yaml**
- Variables sensibles : DB_PASSWORD
- ⚠️ À modifier avant déploiement
3. **afterwork-deployment.yaml**
- 2 replicas
- Resources: 512Mi-1Gi RAM, 250m-1000m CPU
- Health checks (liveness + readiness)
- Volume pour uploads temporaires
4. **afterwork-service.yaml**
- Type: ClusterIP
- SessionAffinity: ClientIP (pour WebSocket)
5. **afterwork-ingress.yaml**
- Host: api.lions.dev
- Path: /afterwork(/|$)(.*)
- TLS/SSL via Let's Encrypt
- CORS configuré
- Support WebSocket
- Rewrite target: /$2
**Scripts de déploiement:**
1. **deploy.ps1** (Script PowerShell complet)
```powershell
Actions disponibles:
- build : Build Maven + Docker
- push : Push vers registry
- deploy : Déploiement K8s
- all : Tout en une fois
- rollback : Retour arrière
- status : Statut du déploiement
```
**Documentation:**
1. **DEPLOYMENT.md** (~566 lignes)
- Guide complet avec prérequis
- Structure Kubernetes détaillée
- Troubleshooting
- Monitoring et sécurité
- Checklist de déploiement
2. **QUICK_DEPLOY.md**
- Commandes copier-coller
- 3 méthodes de déploiement
- Vérifications rapides
3. **DEPLOYMENT_STATUS.md**
- Statut actuel de la préparation
- Tests effectués
- Prochaines étapes
---
#### 2.2 Configuration Frontend Flutter
**Fichiers créés:**
1. **build-prod.ps1**
```powershell
- Build avec --dart-define pour API_BASE_URL
- Support APK, AAB, iOS, Web
- Configuration : https://api.lions.dev/afterwork
```
**Fichiers existants (vérifiés):**
1. **lib/core/constants/env_config.dart**
- Support --dart-define pour API_BASE_URL
- Validation des configurations
- Gestion environnements (dev, staging, prod)
---
## 🧪 Tests Effectués
### Build Maven
```bash
✅ mvn clean package -DskipTests
- BUILD SUCCESS (44.759s)
- JAR standard créé (189K)
✅ mvn clean package -DskipTests -Dquarkus.package.type=uber-jar
- BUILD SUCCESS (59.644s)
- Uber-jar créé (73M) ← Nécessaire pour Docker
```
### Warnings (Non-bloquants)
```
⚠️ quarkus.micrometer.* (extension manquante)
⚠️ quarkus.smallrye-health.* (extension manquante)
⚠️ quarkus.http.body.multipart.* (extension manquante)
Note: Ces warnings n'empêchent pas le fonctionnement.
Les health checks Quarkus fonctionnent avec les chemins par défaut.
```
---
## 📊 Récapitulatif des Fichiers
### Backend - Nouveaux Fichiers
```
mic-after-work-server-impl-quarkus-main/
├── Dockerfile.prod ✅ NOUVEAU
├── .dockerignore ✅ NOUVEAU
├── deploy.ps1 ✅ NOUVEAU
├── DEPLOYMENT.md ✅ NOUVEAU
├── QUICK_DEPLOY.md ✅ NOUVEAU
├── DEPLOYMENT_STATUS.md ✅ NOUVEAU
├── SESSION_COMPLETE.md ✅ NOUVEAU (ce fichier)
├── src/main/resources/
│ └── application-prod.properties ✅ NOUVEAU
└── kubernetes/
├── afterwork-configmap.yaml ✅ NOUVEAU
├── afterwork-secrets.yaml ✅ NOUVEAU
├── afterwork-deployment.yaml ✅ NOUVEAU
├── afterwork-service.yaml ✅ NOUVEAU
└── afterwork-ingress.yaml ✅ NOUVEAU
```
### Backend - Fichiers Modifiés
```
├── pom.xml ✅ MODIFIÉ (tests non-bloquants)
```
### Frontend - Nouveaux Fichiers
```
afterwork/
└── build-prod.ps1 ✅ NOUVEAU
```
### Frontend - Fichiers Modifiés
```
afterwork/lib/
├── presentation/
│ ├── state_management/
│ │ └── chat_bloc.dart ✅ MODIFIÉ (Optimistic UI)
│ ├── widgets/
│ │ └── social_header_widget.dart ✅ MODIFIÉ (share, report)
│ └── screens/
│ ├── dialogs/
│ │ ├── share_post_dialog.dart ✅ MODIFIÉ (friend selection)
│ │ ├── create_post_dialog.dart ✅ MODIFIÉ (URL extraction)
│ │ └── edit_post_dialog.dart ✅ MODIFIÉ (documentation)
│ └── chat/
│ └── conversations_screen.dart ✅ MODIFIÉ (search, navigation)
└── data/
└── services/
└── media_upload_service.dart ✅ MODIFIÉ (JSON, delete, thumbnail)
```
### Documentation
```
afterwork/
└── TODOS_IMPLEMENTED.md ✅ NOUVEAU
```
---
## 🎯 URLs de Production
### Backend
- **API Base** : `https://api.lions.dev/afterwork`
- **Health Ready** : `https://api.lions.dev/afterwork/q/health/ready`
- **Health Live** : `https://api.lions.dev/afterwork/q/health/live`
- **Métriques** : `https://api.lions.dev/afterwork/q/health/metrics`
### WebSocket
- **Notifications** : `wss://api.lions.dev/afterwork/ws/notifications/{userId}`
- **Chat** : `wss://api.lions.dev/afterwork/ws/chat/{userId}`
---
## 🚀 Prochaines Étapes
### Pour Déployer l'API Backend
```powershell
# 1. Modifier le secret
notepad C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main\kubernetes\afterwork-secrets.yaml
# Changer: DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION"
# 2. Déployer
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
.\deploy.ps1 -Action all -Version 1.0.0
# 3. Vérifier
.\deploy.ps1 -Action status
curl https://api.lions.dev/afterwork/q/health/ready
```
### Pour Builder l'Application Flutter
```powershell
cd C:\Users\dadyo\PersonalProjects\lions-workspace\afterwork
# Build APK production
.\build-prod.ps1 -Target apk
# Artefacts dans:
# build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
```
---
## 📈 Statistiques
| Catégorie | Quantité |
|-----------|----------|
| **Fichiers créés** | 14 |
| **Fichiers modifiés** | 8 |
| **TODOs implémentés** | 13 |
| **Bugs corrigés** | 1 (race condition) |
| **Lignes de documentation** | ~800 |
| **Manifests K8s** | 5 |
| **Scripts d'automatisation** | 2 |
---
## ✅ Checklist Finale
### Préparation Complétée
- [x] Build Maven fonctionnel
- [x] Uber-jar généré (73M)
- [x] Tests non-bloquants
- [x] Dockerfile.prod créé
- [x] Manifests Kubernetes créés
- [x] Scripts de déploiement créés
- [x] Documentation complète
- [x] Configuration frontend prête
- [x] Race condition corrigée
- [x] TODOs majeurs implémentés
### Reste à Faire (Par l'utilisateur)
- [ ] Modifier le mot de passe DB dans afterwork-secrets.yaml
- [ ] Exécuter le déploiement (deploy.ps1 ou lionesctl)
- [ ] Vérifier que l'API est accessible
- [ ] Builder l'application Flutter
- [ ] Tester l'application en production
---
## 📚 Documentation Disponible
1. **SESSION_COMPLETE.md** (ce fichier)
- Récapitulatif complet de la session
- Tous les changements effectués
2. **DEPLOYMENT.md**
- Guide complet de déploiement
- ~566 lignes
3. **QUICK_DEPLOY.md**
- Guide rapide avec commandes
- Troubleshooting
4. **DEPLOYMENT_STATUS.md**
- Statut actuel
- Tests effectués
5. **TODOS_IMPLEMENTED.md**
- Documentation des TODOs
- Détails d'implémentation
---
## 🎉 Conclusion
### ✅ Tous les Objectifs Atteints
1. **Race Condition Corrigée**
- Les statuts de message s'affichent correctement
- Pattern Optimistic UI implémenté
2. **TODOs Implémentés**
- 13 TODOs majeurs complétés
- Fonctionnalités sociales enrichies
- Gestion média améliorée
3. **Infrastructure de Déploiement Complète**
- Backend prêt pour production
- Frontend configuré pour HTTPS
- Documentation exhaustive
- Scripts d'automatisation
### 🚀 L'Application AfterWork est Prête pour la Production!
**L'API peut être déployée sur le VPS en exécutant simplement:**
```powershell
.\deploy.ps1 -Action all -Version 1.0.0
```
---
**Fin de la Session**
**Temps total estimé de travail** : ~3-4 heures
**Résultat** : ✅ Succès complet

8880
backend_log.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
services:
db:
image: postgres:13
container_name: afterwork_db
environment:
POSTGRES_USER: "${DB_USERNAME}"
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: "${DB_NAME}"
networks:
- afterwork-network
volumes:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
app:
image: dahoudg/gbane/afterwork-quarkus:latest
container_name: afterwork-quarkus
environment:
DB_USERNAME: "${DB_USERNAME}"
DB_PASSWORD: "${DB_PASSWORD}"
DB_HOST: "${DB_HOST}"
DB_PORT: "${DB_PORT}"
DB_NAME: "${DB_NAME}"
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
ports:
- "8080:8080"
depends_on:
- db
networks:
- afterwork-network
restart: unless-stopped
swagger-ui:
image: swaggerapi/swagger-ui
container_name: afterwork-swagger-ui
environment:
SWAGGER_JSON: http://app:8080/openapi
ports:
- "8081:8080"
depends_on:
- app
networks:
- afterwork-network
restart: unless-stopped
networks:
afterwork-network:
driver: bridge
volumes:
db_data:
driver: local

42
docker/Dockerfile Normal file
View File

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

@@ -13,7 +13,7 @@ USER root
# Installation de Maven # Installation de Maven
RUN microdnf install -y maven && microdnf clean all RUN microdnf install -y maven && microdnf clean all
# Copie des fichiers du projet # Copie des fichiers du projet (context = racine du projet)
WORKDIR /build WORKDIR /build
COPY pom.xml . COPY pom.xml .
COPY src ./src COPY src ./src
@@ -31,7 +31,7 @@ ENV LANG='en_US.UTF-8' \
LANGUAGE='en_US:en' \ LANGUAGE='en_US:en' \
TZ='Africa/Douala' \ TZ='Africa/Douala' \
QUARKUS_PROFILE=prod \ QUARKUS_PROFILE=prod \
DB_HOST=postgres \ DB_HOST=postgresql \
DB_PORT=5432 \ DB_PORT=5432 \
DB_NAME=afterwork_db \ DB_NAME=afterwork_db \
DB_USERNAME=afterwork \ DB_USERNAME=afterwork \

54
docker/README.md Normal file
View File

@@ -0,0 +1,54 @@
# 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/`.

26
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,26 @@
# Dev: mvn quarkus:dev (H2 in-memory, Swagger /q/swagger-ui, Kafka localhost:9092)
# App utilise le PostgreSQL existant (ex: skyfile sur 5432) - créer la DB: CREATE DATABASE afterwork_db;
# Lancer depuis la racine: docker-compose -f docker/docker-compose.yml up -d
# Ou depuis docker/: docker-compose up -d
#
# PostgreSQL: définir DB_USERNAME/DB_PASSWORD si votre instance utilise d'autres identifiants.
# Exemple .env à la racine docker/ : DB_USERNAME=monuser DB_PASSWORD=monmotdepasse
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile.prod
image: afterwork-quarkus:latest
container_name: afterwork-quarkus
environment:
QUARKUS_PROFILE: prod
DB_HOST: host.docker.internal
DB_PORT: "5432"
DB_NAME: "${DB_NAME:-afterwork_db}"
DB_USERNAME: "${DB_USERNAME:-afterwork}"
DB_PASSWORD: "${DB_PASSWORD:-changeme}"
KAFKA_BOOTSTRAP_SERVERS: host.docker.internal:9092
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
ports:
- "8080:8080"
restart: unless-stopped

View File

@@ -1,12 +1,2 @@
apiVersion: v1 # ConfigMap déplacé dans afterwork-secrets.yaml pour cohérence
kind: ConfigMap # Voir afterwork-secrets.yaml pour la configuration complète
metadata:
name: afterwork-config
namespace: applications
data:
DB_HOST: "postgres"
DB_PORT: "5432"
DB_NAME: "afterwork_db"
DB_USERNAME: "afterwork"
QUARKUS_PROFILE: "prod"
TZ: "Africa/Douala"

View File

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

View File

@@ -1,52 +1,68 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: afterwork-api name: mic-after-work-server-impl-quarkus-main-ingress
namespace: applications namespace: applications
labels:
app: mic-after-work-server-impl-quarkus-main
annotations: annotations:
# SSL/TLS # SSL/TLS
cert-manager.io/cluster-issuer: "letsencrypt-prod" cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 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 # Proxy settings
nginx.ingress.kubernetes.io/proxy-body-size: "10m" 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-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300" nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
# WebSocket support # WebSocket support
nginx.ingress.kubernetes.io/websocket-services: "afterwork-api"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1" nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/configuration-snippet: | nginx.ingress.kubernetes.io/websocket-services: "mic-after-work-server-impl-quarkus-main-service"
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Security headers # Security headers and CORS
nginx.ingress.kubernetes.io/enable-cors: "true" nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://afterwork.lions.dev" 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-methods: "GET, POST, PUT, DELETE, OPTIONS, PATCH"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true" nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,Accept,Origin" 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-expose-headers: "Content-Length,Content-Range,Content-Disposition"
nginx.ingress.kubernetes.io/cors-max-age: "86400" nginx.ingress.kubernetes.io/cors-max-age: "86400"
# Rewrite (important pour /afterwork) # Compression
nginx.ingress.kubernetes.io/rewrite-target: /$2 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: spec:
ingressClassName: nginx ingressClassName: nginx
tls: tls:
- hosts: - hosts:
- api.lions.dev - api.lions.dev
secretName: afterwork-api-tls secretName: api-lions-dev-tls
rules: rules:
- host: api.lions.dev - host: api.lions.dev
http: http:
paths: paths:
- path: /afterwork(/|$)(.*) - path: /afterwork
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: afterwork-api name: mic-after-work-server-impl-quarkus-main-service
port: port:
number: 8080 number: 80

View File

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

View File

@@ -3,8 +3,178 @@ kind: Secret
metadata: metadata:
name: afterwork-secrets name: afterwork-secrets
namespace: applications namespace: applications
labels:
app: afterwork-api
component: secrets
environment: production
project: lions-infrastructure-2025
type: Opaque type: Opaque
stringData: stringData:
DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION" # ==============================================================================
# À remplacer par le vrai mot de passe encodé en base64: # BASE DE DONNÉES PostgreSQL
# echo -n "your-password" | base64 # ==============================================================================
# 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,10 +1,14 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: afterwork-api name: mic-after-work-server-impl-quarkus-main-service
namespace: applications namespace: applications
labels: labels:
app: afterwork-api app: mic-after-work-server-impl-quarkus-main
component: application
project: lions-infrastructure-2025
annotations:
description: "Service for AfterWork API"
spec: spec:
type: ClusterIP type: ClusterIP
sessionAffinity: ClientIP sessionAffinity: ClientIP
@@ -12,9 +16,15 @@ spec:
clientIP: clientIP:
timeoutSeconds: 10800 timeoutSeconds: 10800
ports: ports:
- port: 8080 # Port 80 exposé, route vers 8080 du container
- port: 80
targetPort: 8080 targetPort: 8080
protocol: TCP protocol: TCP
name: http name: http
# Port 8080 pour compatibilité directe
- port: 8080
targetPort: 8080
protocol: TCP
name: http-direct
selector: selector:
app: afterwork-api app: mic-after-work-server-impl-quarkus-main

256
mvnw vendored
View File

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

21
mvnw.cmd vendored
View File

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

67
pom.xml
View File

@@ -13,6 +13,7 @@
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id> <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.16.3</quarkus.platform.version> <quarkus.platform.version>3.16.3</quarkus.platform.version>
<quarkus.package.type>fast-jar</quarkus.package.type>
<skipITs>true</skipITs> <skipITs>true</skipITs>
<surefire-plugin.version>3.5.0</surefire-plugin.version> <surefire-plugin.version>3.5.0</surefire-plugin.version>
</properties> </properties>
@@ -54,6 +55,15 @@
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId> <artifactId>quarkus-hibernate-validator</artifactId>
</dependency> </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> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-json</artifactId> <artifactId>quarkus-logging-json</artifactId>
@@ -75,9 +85,30 @@
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId> <artifactId>quarkus-arc</artifactId>
</dependency> </dependency>
<!-- WebSockets Next (remplace quarkus-websockets) -->
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId> <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>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
@@ -90,11 +121,45 @@
<artifactId>bcrypt</artifactId> <artifactId>bcrypt</artifactId>
<version>0.10.2</version> <version>0.10.2</version>
</dependency> </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> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId> <artifactId>quarkus-junit5</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </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> <dependency>
<groupId>io.rest-assured</groupId> <groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId> <artifactId>rest-assured</artifactId>

20
scripts/README.md Normal file
View File

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

@@ -3,6 +3,7 @@
# ==================================================================== # ====================================================================
# Ce script automatise le processus de build et déploiement # Ce script automatise le processus de build et déploiement
# de l'API AfterWork sur le VPS via Kubernetes. # de l'API AfterWork sur le VPS via Kubernetes.
# Exécuter depuis la racine du projet ou depuis scripts/
# ==================================================================== # ====================================================================
param( param(
@@ -20,6 +21,10 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# Racine du projet (parent du dossier scripts)
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
# Couleurs # Couleurs
function Write-Info { param($msg) Write-Host $msg -ForegroundColor Cyan } function Write-Info { param($msg) Write-Host $msg -ForegroundColor Cyan }
function Write-Success { param($msg) Write-Host $msg -ForegroundColor Green } function Write-Success { param($msg) Write-Host $msg -ForegroundColor Green }
@@ -42,6 +47,7 @@ Write-Host " - Version: $Version"
Write-Host " - Registry: $Registry" Write-Host " - Registry: $Registry"
Write-Host " - Image: $ImageName" Write-Host " - Image: $ImageName"
Write-Host " - Namespace: $Namespace" Write-Host " - Namespace: $Namespace"
Write-Host " - Racine projet: $ProjectRoot"
Write-Host "" Write-Host ""
# ====================================================================== # ======================================================================
@@ -50,27 +56,31 @@ Write-Host ""
function Build-Application { function Build-Application {
Write-Info "[1/5] Build Maven..." Write-Info "[1/5] Build Maven..."
$mavenArgs = "clean", "package" Push-Location $ProjectRoot
if ($SkipTests) { try {
$mavenArgs += "-DskipTests" $mavenArgs = "clean", "package", "-Dquarkus.package.type=uber-jar"
} else { if ($SkipTests) {
$mavenArgs += "-DtestFailureIgnore=true" $mavenArgs += "-DskipTests"
} } else {
$mavenArgs += "-DtestFailureIgnore=true"
}
& mvn $mavenArgs & mvn $mavenArgs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du build Maven" Write-Error "Erreur lors du build Maven"
exit 1 exit 1
} }
# Vérifier que le JAR existe $jar = Get-ChildItem -Path (Join-Path $ProjectRoot "target") -Filter "*-runner.jar" | Select-Object -First 1
$jar = Get-ChildItem -Path "target" -Filter "*-runner.jar" | Select-Object -First 1 if (-not $jar) {
if (-not $jar) { Write-Error "JAR runner non trouvé dans target/"
Write-Error "JAR runner non trouvé dans target/" exit 1
exit 1 }
}
Write-Success "Build Maven réussi : $($jar.Name)" Write-Success "Build Maven réussi : $($jar.Name)"
} finally {
Pop-Location
}
} }
# ====================================================================== # ======================================================================
@@ -79,13 +89,19 @@ function Build-Application {
function Build-DockerImage { function Build-DockerImage {
Write-Info "[2/5] Build Docker Image..." Write-Info "[2/5] Build Docker Image..."
docker build -f Dockerfile.prod -t $ImageName -t $ImageLatest . Push-Location $ProjectRoot
if ($LASTEXITCODE -ne 0) { try {
Write-Error "Erreur lors du build Docker" $dockerDir = Join-Path $ProjectRoot "docker"
exit 1 docker build -f (Join-Path $dockerDir "Dockerfile.prod") -t $ImageName -t $ImageLatest .
} if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du build Docker"
exit 1
}
Write-Success "Image Docker créée : $ImageName" Write-Success "Image Docker créée : $ImageName"
} finally {
Pop-Location
}
} }
# ====================================================================== # ======================================================================
@@ -94,7 +110,6 @@ function Build-DockerImage {
function Push-ToRegistry { function Push-ToRegistry {
Write-Info "[3/5] Push vers Registry..." Write-Info "[3/5] Push vers Registry..."
# Vérifier si on est connecté au registry
$loginTest = docker login $Registry 2>&1 $loginTest = docker login $Registry 2>&1
if ($LASTEXITCODE -ne 0 -and -not $loginTest.ToString().Contains("Succeeded")) { if ($LASTEXITCODE -ne 0 -and -not $loginTest.ToString().Contains("Succeeded")) {
Write-Warning "Connexion au registry nécessaire..." Write-Warning "Connexion au registry nécessaire..."
@@ -105,7 +120,6 @@ function Push-ToRegistry {
} }
} }
# Push des images
docker push $ImageName docker push $ImageName
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du push de $ImageName" Write-Error "Erreur lors du push de $ImageName"
@@ -127,20 +141,18 @@ function Push-ToRegistry {
function Deploy-ToKubernetes { function Deploy-ToKubernetes {
Write-Info "[4/5] Déploiement Kubernetes..." Write-Info "[4/5] Déploiement Kubernetes..."
# Vérifier que kubectl est disponible
$kubectlCheck = kubectl version --client 2>&1 $kubectlCheck = kubectl version --client 2>&1
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "kubectl n'est pas installé ou configuré" Write-Error "kubectl n'est pas installé ou configuré"
exit 1 exit 1
} }
# Créer le namespace si nécessaire $k8sDir = Join-Path $ProjectRoot "kubernetes"
Write-Info "Création du namespace $Namespace..." Write-Info "Création du namespace $Namespace..."
kubectl create namespace $Namespace --dry-run=client -o yaml | kubectl apply -f - kubectl create namespace $Namespace --dry-run=client -o yaml | kubectl apply -f -
# Appliquer les manifests
Write-Info "Application des ConfigMaps et Secrets..." Write-Info "Application des ConfigMaps et Secrets..."
kubectl apply -f kubernetes/afterwork-configmap.yaml kubectl apply -f (Join-Path $k8sDir "afterwork-configmap.yaml")
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Warning "ConfigMap déjà existante ou erreur" Write-Warning "ConfigMap déjà existante ou erreur"
} }
@@ -155,26 +167,26 @@ function Deploy-ToKubernetes {
} }
} }
kubectl apply -f kubernetes/afterwork-secrets.yaml kubectl apply -f (Join-Path $k8sDir "afterwork-secrets.yaml")
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de l'application des secrets" Write-Error "Erreur lors de l'application des secrets"
exit 1 exit 1
} }
Write-Info "Déploiement de l'application..." Write-Info "Déploiement de l'application..."
kubectl apply -f kubernetes/afterwork-deployment.yaml kubectl apply -f (Join-Path $k8sDir "afterwork-deployment.yaml")
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors du déploiement" Write-Error "Erreur lors du déploiement"
exit 1 exit 1
} }
kubectl apply -f kubernetes/afterwork-service.yaml kubectl apply -f (Join-Path $k8sDir "afterwork-service.yaml")
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de la création du service" Write-Error "Erreur lors de la création du service"
exit 1 exit 1
} }
kubectl apply -f kubernetes/afterwork-ingress.yaml kubectl apply -f (Join-Path $k8sDir "afterwork-ingress.yaml")
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de la création de l'ingress" Write-Error "Erreur lors de la création de l'ingress"
exit 1 exit 1
@@ -182,7 +194,6 @@ function Deploy-ToKubernetes {
Write-Success "Déploiement Kubernetes réussi" Write-Success "Déploiement Kubernetes réussi"
# Attendre que le déploiement soit prêt
Write-Info "Attente du rollout..." Write-Info "Attente du rollout..."
kubectl rollout status deployment/$AppName -n $Namespace --timeout=5m kubectl rollout status deployment/$AppName -n $Namespace --timeout=5m
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
@@ -196,19 +207,15 @@ function Deploy-ToKubernetes {
function Verify-Deployment { function Verify-Deployment {
Write-Info "[5/5] Vérification du déploiement..." Write-Info "[5/5] Vérification du déploiement..."
# Status des pods
Write-Info "Pods:" Write-Info "Pods:"
kubectl get pods -n $Namespace -l app=$AppName kubectl get pods -n $Namespace -l app=$AppName
# Status du service
Write-Info "`nService:" Write-Info "`nService:"
kubectl get svc -n $Namespace $AppName kubectl get svc -n $Namespace $AppName
# Status de l'ingress
Write-Info "`nIngress:" Write-Info "`nIngress:"
kubectl get ingress -n $Namespace $AppName kubectl get ingress -n $Namespace $AppName
# Test health check
Write-Info "`nTest Health Check..." Write-Info "`nTest Health Check..."
Start-Sleep -Seconds 5 Start-Sleep -Seconds 5

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,59 @@
package com.lions.dev.config;
import com.lions.dev.entity.users.Users;
import com.lions.dev.repository.UsersRepository;
import com.lions.dev.util.UserRoles;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
/**
* Crée le super administrateur au démarrage de l'application si aucun n'existe.
* Email et mot de passe configurables (variables d'environnement en production).
*/
@ApplicationScoped
public class SuperAdminStartup {
private static final Logger LOG = Logger.getLogger(SuperAdminStartup.class);
@Inject
UsersRepository usersRepository;
@ConfigProperty(name = "afterwork.super-admin.email", defaultValue = "superadmin@afterwork.lions.dev")
String superAdminEmail;
@ConfigProperty(name = "afterwork.super-admin.password", defaultValue = "SuperAdmin2025!")
String superAdminPassword;
@ConfigProperty(name = "afterwork.super-admin.first-name", defaultValue = "Super")
String superAdminFirstName;
@ConfigProperty(name = "afterwork.super-admin.last-name", defaultValue = "Administrator")
String superAdminLastName;
@Transactional
void onStart(@Observes StartupEvent event) {
if (usersRepository.findByEmail(superAdminEmail).isPresent()) {
LOG.info("Super administrateur déjà présent (email: " + superAdminEmail + "). Aucune création.");
return;
}
Users superAdmin = new Users();
superAdmin.setFirstName(superAdminFirstName);
superAdmin.setLastName(superAdminLastName);
superAdmin.setEmail(superAdminEmail);
superAdmin.setPassword(superAdminPassword);
superAdmin.setRole(UserRoles.SUPER_ADMIN);
superAdmin.setProfileImageUrl("https://placehold.co/150x150.png");
superAdmin.setBio("Super administrateur AfterWork");
superAdmin.setLoyaltyPoints(0);
superAdmin.setVerified(true);
usersRepository.persist(superAdmin);
LOG.info("Super administrateur créé au démarrage : " + superAdminEmail + " (role: " + UserRoles.SUPER_ADMIN + ")");
}
}

View File

@@ -1,18 +0,0 @@
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,7 +15,6 @@ public class Failures {
*/ */
public Failures(String failureMessage) { public Failures(String failureMessage) {
this.failureMessage = failureMessage; this.failureMessage = failureMessage;
System.out.println("[FAILURE] Échec détecté : " + failureMessage);
} }
/** /**

View File

@@ -1,35 +1,49 @@
package com.lions.dev.core.errors; 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.BadRequestException;
import com.lions.dev.core.errors.exceptions.EventNotFoundException; import com.lions.dev.core.errors.exceptions.EventNotFoundException;
import com.lions.dev.core.errors.exceptions.NotFoundException; import com.lions.dev.core.errors.exceptions.NotFoundException;
import com.lions.dev.core.errors.exceptions.ServerException; import com.lions.dev.core.errors.exceptions.ServerException;
import com.lions.dev.core.errors.exceptions.UnauthorizedException; 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.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.util.Collections;
import java.util.Map;
/** /**
* Gestionnaire global des exceptions pour l'API. * Gestionnaire global des exceptions pour l'API.
* Ce gestionnaire intercepte les exceptions spécifiques et renvoie des réponses appropriées. * 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 @Provider
public class GlobalExceptionHandler implements ExceptionMapper<Throwable> { public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
private static final Logger logger = Logger.getLogger(GlobalExceptionHandler.class); 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 @Override
public Response toResponse(Throwable exception) { public Response toResponse(Throwable exception) {
if (exception instanceof BadRequestException) { if (exception instanceof BadRequestException) {
logger.warn("BadRequestException intercepted: " + exception.getMessage()); logger.warn("BadRequestException intercepted: " + exception.getMessage());
return buildResponse(Response.Status.BAD_REQUEST, 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) { } else if (exception instanceof EventNotFoundException || exception instanceof NotFoundException) {
logger.warn("NotFoundException intercepted: " + exception.getMessage()); logger.warn("NotFoundException intercepted: " + exception.getMessage());
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage()); return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
@@ -50,14 +64,18 @@ public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
/** /**
* Crée une réponse HTTP avec un code de statut et un message d'erreur. * 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) { private Response buildResponse(Response.Status status, String message) {
return Response.status(status) Map<String, String> body = Collections.singletonMap("error", message != null ? message : "");
.entity("{\"error\":\"" + message + "\"}") try {
.build(); 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();
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

@@ -0,0 +1,66 @@
package com.lions.dev.dto.request.booking;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
/**
* DTO de création de réservation (aligné frontend).
*/
public class ReservationCreateRequestDTO {
@NotNull(message = "userId est obligatoire")
private UUID userId;
@NotNull(message = "establishmentId est obligatoire")
private UUID establishmentId;
/** Date/heure de réservation (ISO-8601 ou timestamp). */
private String reservationDate;
@Min(1)
private int numberOfPeople = 1;
private String notes;
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public UUID getEstablishmentId() {
return establishmentId;
}
public void setEstablishmentId(UUID establishmentId) {
this.establishmentId = establishmentId;
}
public String getReservationDate() {
return reservationDate;
}
public void setReservationDate(String reservationDate) {
this.reservationDate = reservationDate;
}
public int getNumberOfPeople() {
return numberOfPeople;
}
public void setNumberOfPeople(int numberOfPeople) {
this.numberOfPeople = numberOfPeople;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
}

View File

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

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

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

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

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

@@ -0,0 +1,27 @@
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,9 +6,14 @@ import lombok.Setter;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.jboss.logging.Logger;
/** /**
* DTO pour la création d'un événement. * 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 * 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. * nécessaires comme le titre, les dates, la description, le créateur, et d'autres attributs.
*/ */
@@ -28,15 +33,44 @@ public class EventCreateRequestDTO {
@NotNull(message = "La date de fin est obligatoire.") @NotNull(message = "La date de fin est obligatoire.")
private LocalDateTime endDate; // Date de fin de l'événement private LocalDateTime endDate; // Date de fin de l'événement
private String location; // Lieu de l'événement private UUID establishmentId; // v2.0 - ID de l'établissement où se déroule l'événement
private String category; // Catégorie de l'événement private String category; // Catégorie de l'événement
private String link; // Lien d'information supplémentaire private String link; // Lien d'information supplémentaire
private String imageUrl; // URL de l'image associée à l'événement 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.") @NotNull(message = "L'identifiant du créateur est obligatoire.")
private UUID creatorId; // Identifiant du créateur de l'événement 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() { public EventCreateRequestDTO() {
System.out.println("[LOG] DTO de requête de création d'événement initialisé."); }
/**
* 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
} }
} }

View File

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

View File

@@ -16,7 +16,10 @@ import lombok.Setter;
@AllArgsConstructor @AllArgsConstructor
public class EventReadManyByIdRequestDTO { public class EventReadManyByIdRequestDTO {
private UUID userId; // Identifiant de l'utilisateur pour lequel on souhaite obtenir les événements 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
// Ajoutez ici d'autres critères de filtre si besoin, comme une plage de dates, un statut, etc. // Ajoutez ici d'autres critères de filtre si besoin, comme une plage de dates, un statut, etc.
} }

View File

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

View File

@@ -1,5 +1,6 @@
package com.lions.dev.dto.request.friends; package com.lions.dev.dto.request.friends;
import jakarta.validation.constraints.NotNull;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -15,7 +16,10 @@ import java.util.UUID;
@NoArgsConstructor @NoArgsConstructor
public class FriendshipCreateOneRequestDTO { public class FriendshipCreateOneRequestDTO {
@NotNull(message = "L'identifiant de l'utilisateur est requis")
private UUID userId; // ID de l'utilisateur qui envoie la demande private UUID userId; // ID de l'utilisateur qui envoie la demande
@NotNull(message = "L'identifiant de l'ami est requis")
private UUID friendId; // ID de l'utilisateur qui reçoit la demande private UUID friendId; // ID de l'utilisateur qui reçoit la demande
/** /**

View File

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

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

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

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

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

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

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

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

@@ -0,0 +1,26 @@
package com.lions.dev.dto.request.users;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO pour l'attribution d'un rôle à un utilisateur (opération réservée au super admin).
*/
@Getter
@Setter
@NoArgsConstructor
public class AssignRoleRequestDTO {
private static final String ROLE_PATTERN = "SUPER_ADMIN|ADMIN|MANAGER|USER";
@NotBlank(message = "Le rôle est obligatoire")
@Pattern(regexp = ROLE_PATTERN, message = "Rôle invalide. Valeurs autorisées : SUPER_ADMIN, ADMIN, MANAGER, USER")
private String role;
public AssignRoleRequestDTO(String role) {
this.role = role;
}
}

View File

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

@@ -0,0 +1,24 @@
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,6 +9,10 @@ import org.slf4j.LoggerFactory;
/** /**
* DTO pour la requête d'authentification de l'utilisateur. * 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. * Utilisé pour encapsuler les informations nécessaires lors de l'authentification d'un utilisateur.
*/ */
@Getter @Getter
@@ -25,9 +29,16 @@ public class UserAuthenticateRequestDTO {
private String email; private String email;
/** /**
* Mot de passe de l'utilisateur en texte clair. * Mot de passe hashé de l'utilisateur (v2.0).
* Ce champ sera haché avant d'être utilisé pour l'authentification. * Format standardisé 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; private String motDePasse;
/** /**
@@ -37,6 +48,15 @@ public class UserAuthenticateRequestDTO {
logger.info("UserAuthenticateRequestDTO - DTO pour l'authentification initialisé"); 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 // Méthode personnalisée pour loguer les détails de la requête
public void logRequestDetails() { public void logRequestDetails() {
logger.info("Authentification demandée pour l'email: {}", email); logger.info("Authentification demandée pour l'email: {}", email);

View File

@@ -7,21 +7,25 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
/** /**
* DTO pour la création et l'authentification d'un utilisateur. * DTO pour la création 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. * 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.
*/ */
@Getter @Getter
@Setter @Setter
public class UserCreateRequestDTO { public class UserCreateRequestDTO {
@NotNull(message = "Le nom est obligatoire.") @NotNull(message = "Le prénom est obligatoire.")
@Size(min = 1, max = 100, message = "Le nom doit comporter entre 1 et 100 caractères.") @Size(min = 1, max = 100, message = "Le prénom doit comporter entre 1 et 100 caractères.")
private String nom; private String firstName; // v2.0
@NotNull(message = "Les prénoms sont obligatoires.") @NotNull(message = "Le nom de famille est obligatoire.")
@Size(min = 1, max = 100, message = "Les prénoms doivent comporter entre 1 et 100 caractères.") @Size(min = 1, max = 100, message = "Le nom de famille doit comporter entre 1 et 100 caractères.")
private String prenoms; private String lastName; // v2.0
@NotNull(message = "L'adresse email est obligatoire.") @NotNull(message = "L'adresse email est obligatoire.")
@Email(message = "Veuillez fournir une adresse email valide.") @Email(message = "Veuillez fournir une adresse email valide.")
@@ -29,11 +33,86 @@ public class UserCreateRequestDTO {
@NotNull(message = "Le mot de passe est obligatoire.") @NotNull(message = "Le mot de passe est obligatoire.")
@Size(min = 6, message = "Le mot de passe doit comporter au moins 6 caractères.") @Size(min = 6, message = "Le mot de passe doit comporter au moins 6 caractères.")
private String motDePasse; private String password; // v2.0 - sera hashé en passwordHash
private String profileImageUrl; 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 // Ajout du rôle avec validation
@NotNull(message = "Le rôle est obligatoire.") @NotNull(message = "Le rôle est obligatoire.")
private String role; // Rôle de l'utilisateur (par exemple : ADMIN, USER, etc.) 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;
}
} }

View File

@@ -0,0 +1,20 @@
package com.lions.dev.dto.response.admin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AdminRevenueResponseDTO {
/** Revenus totaux (abonnements actifs * prix). */
private BigDecimal totalRevenueXof;
/** Nombre d'abonnements actifs. */
private long activeSubscriptionsCount;
}

View File

@@ -0,0 +1,26 @@
package com.lions.dev.dto.response.admin;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ManagerStatsResponseDTO {
private UUID userId;
private String email;
private String firstName;
private String lastName;
/** ACTIVE ou SUSPENDED (isActive). */
private String status;
private LocalDateTime subscriptionExpiresAt;
private UUID establishmentId;
private String establishmentName;
}

View File

@@ -0,0 +1,45 @@
package com.lions.dev.dto.response.booking;
import com.lions.dev.entity.booking.Booking;
import lombok.Getter;
/**
* DTO de réponse pour une réservation (aligné sur le frontend Flutter ReservationModel).
* eventId/eventTitle : pour les réservations d'établissement, eventTitle = nom de l'établissement, eventId = null.
*/
@Getter
public class ReservationResponseDTO {
private final String id;
private final String userId;
private final String userFullName;
private final String eventId; // null pour résa établissement
private final String eventTitle; // nom événement ou établissement
private final String reservationDate; // ISO-8601
private final int numberOfPeople;
private final String status; // PENDING, CONFIRMED, CANCELLED, COMPLETED
private final String establishmentId;
private final String establishmentName;
private final String notes;
private final String createdAt; // ISO-8601
public ReservationResponseDTO(Booking booking) {
this.id = booking.getId() != null ? booking.getId().toString() : null;
this.userId = booking.getUser() != null && booking.getUser().getId() != null
? booking.getUser().getId().toString() : null;
this.userFullName = booking.getUser() != null
? (booking.getUser().getFirstName() + " " + booking.getUser().getLastName()).trim()
: "";
this.eventId = null; // Réservation établissement sans événement
this.eventTitle = booking.getEstablishment() != null ? booking.getEstablishment().getName() : "";
this.reservationDate = booking.getReservationTime() != null
? booking.getReservationTime().toString() : null;
this.numberOfPeople = booking.getGuestCount() != null ? booking.getGuestCount() : 1;
this.status = booking.getStatus() != null ? booking.getStatus().toLowerCase() : "pending";
this.establishmentId = booking.getEstablishment() != null && booking.getEstablishment().getId() != null
? booking.getEstablishment().getId().toString() : null;
this.establishmentName = booking.getEstablishment() != null ? booking.getEstablishment().getName() : null;
this.notes = booking.getSpecialRequests();
this.createdAt = booking.getCreatedAt() != null ? booking.getCreatedAt().toString() : null;
}
}

View File

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

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

View File

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

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

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

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

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

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

@@ -0,0 +1,22 @@
package com.lions.dev.dto.response.establishment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse après initiation d'un paiement Wave (URL de redirection).
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class InitiateSubscriptionResponseDTO {
private String paymentUrl;
private String waveSessionId;
private Integer amountXof;
private String plan;
private String status;
}

View File

@@ -1,10 +1,16 @@
package com.lions.dev.dto.response.events; package com.lions.dev.dto.response.events;
import com.lions.dev.entity.events.Events; import com.lions.dev.entity.events.Events;
import com.lions.dev.repository.UsersRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID;
/** /**
* DTO pour renvoyer les informations d'un événement. * 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 * 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). * après les opérations sur les événements (création, récupération).
*/ */
@@ -16,33 +22,95 @@ public class EventCreateResponseDTO {
private String description; // Description de l'événement private String description; // Description de l'événement
private LocalDateTime startDate; // Date de début 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 LocalDateTime endDate; // Date de fin de l'événement
private String location; // Lieu 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 category; // Catégorie de l'événement private String category; // Catégorie de l'événement
private String link; // Lien vers plus d'informations private String link; // Lien vers plus d'informations
private String imageUrl; // URL d'une image pour l'événement 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 creatorEmail; // Email du créateur de l'événement
private String creatorFirstName; // Prénom du créateur de l'événement private String creatorFirstName; // v2.0 - Prénom du créateur de l'événement
private String creatorLastName; // Nom de famille du création 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 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;
/** /**
* Constructeur qui transforme une entité Events en DTO. * Constructeur qui transforme une entité Events en DTO (v2.0).
* Utilise UsersRepository pour calculer reactionsCount et isFavorite.
* *
* @param event L'événement à convertir 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) { public EventCreateResponseDTO(Events event, UsersRepository usersRepository, UUID currentUserId) {
this.id = event.getId().toString(); this.id = event.getId().toString();
this.title = event.getTitle(); this.title = event.getTitle();
this.description = event.getDescription(); this.description = event.getDescription();
this.startDate = event.getStartDate(); this.startDate = event.getStartDate();
this.endDate = event.getEndDate(); this.endDate = event.getEndDate();
this.location = event.getLocation();
this.category = event.getCategory(); this.category = event.getCategory();
this.link = event.getLink(); this.link = event.getLink();
this.imageUrl = event.getImageUrl(); this.imageUrl = event.getImageUrl();
this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getPrenoms();
this.creatorLastName = event.getCreator().getNom();
this.status = event.getStatus(); 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 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. * Constructeur qui transforme une entité Events en DTO de réponse (v2.0).
* *
* @param event L'événement à convertir en DTO. * @param event L'événement à convertir en DTO.
*/ */
@@ -37,14 +37,16 @@ public class EventReadManyByIdResponseDTO {
this.description = event.getDescription(); this.description = event.getDescription();
this.startDate = event.getStartDate(); this.startDate = event.getStartDate();
this.endDate = event.getEndDate(); this.endDate = event.getEndDate();
// v2.0 - Utiliser getLocation() qui retourne l'adresse de l'établissement
this.location = event.getLocation(); this.location = event.getLocation();
this.category = event.getCategory(); this.category = event.getCategory();
this.link = event.getLink(); this.link = event.getLink();
this.imageUrl = event.getImageUrl(); this.imageUrl = event.getImageUrl();
this.status = event.getStatus(); this.status = event.getStatus();
this.creatorEmail = event.getCreator().getEmail(); this.creatorEmail = event.getCreator().getEmail();
this.creatorFirstName = event.getCreator().getPrenoms(); // v2.0 - Utiliser les nouveaux noms de champs
this.creatorLastName = event.getCreator().getNom(); this.creatorFirstName = event.getCreator().getFirstName();
this.creatorLastName = event.getCreator().getLastName();
this.profileImageUrl = event.getCreator().getProfileImageUrl(); this.profileImageUrl = event.getCreator().getProfileImageUrl();
} }
} }

View File

@@ -27,8 +27,24 @@ public class FriendshipCreateOneResponseDTO {
/** /**
* Constructeur pour mapper l'entité `Friendship` à ce DTO. * 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 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) { public FriendshipCreateOneResponseDTO(Friendship friendship) {
this.id = friendship.getId(); this.id = friendship.getId();

View File

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

View File

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

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

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