Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13d3097b3e | ||
|
|
fc451f025e | ||
|
|
e78423dd16 | ||
|
|
4532a25427 | ||
|
|
806efeb074 | ||
|
|
2a794523b6 | ||
|
|
dd4dbe111e | ||
|
|
a515963a4a | ||
|
|
c31c6174cc | ||
|
|
40de25315c | ||
|
|
950041719e | ||
|
|
6e89295e6b | ||
|
|
7021b7a7ce | ||
|
|
bcbae7c599 | ||
|
|
8f5267d895 | ||
|
|
675e0925b8 | ||
|
|
0240442671 | ||
|
|
9dc9ca591c | ||
|
|
ce89face73 | ||
|
|
9d5e388efa | ||
|
|
c5a65bab5b | ||
|
|
cb8b9da12e | ||
|
|
8cb67f1762 | ||
|
|
b9fc1ee05a | ||
|
|
93c63fd600 | ||
|
|
7dd0969799 | ||
|
|
a5fd9538fe | ||
|
|
7309fcc72d | ||
|
|
c26098b0d4 | ||
|
|
bfb174bcf8 | ||
|
|
0443bd251f | ||
|
|
56d0aad6a6 | ||
|
|
c0b1863467 | ||
|
|
9cf41a3b7e | ||
|
|
9499ecb66a | ||
|
|
f63cc63d9d | ||
|
|
d659416627 | ||
|
|
0dafe9ce7f | ||
|
|
730581a46b | ||
|
|
a09cdfb67d | ||
|
|
044c18fe09 | ||
|
|
093d04c224 | ||
|
|
fd67140961 | ||
|
|
4d6a5630fc |
@@ -1,5 +1,48 @@
|
||||
*
|
||||
!target/*-runner
|
||||
!target/*-runner.jar
|
||||
!target/lib/*
|
||||
!target/quarkus-app/*
|
||||
# Build artifacts
|
||||
# Note: target/ et *.jar sont nécessaires pour le Docker build
|
||||
# car on copie le JAR runner depuis target/
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
.github/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
README.md
|
||||
|
||||
# Tests
|
||||
**/test/
|
||||
**/*Test.java
|
||||
**/*TestCase.java
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Temporaires
|
||||
*.tmp
|
||||
*.temp
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Scripts et Docker (hors contexte utile pour le build)
|
||||
scripts/
|
||||
docker/
|
||||
|
||||
129
.env.example
Normal file
129
.env.example
Normal 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
86
.gitignore
vendored
@@ -1,43 +1,105 @@
|
||||
#Maven
|
||||
# ====================
|
||||
# Maven
|
||||
# ====================
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
release.properties
|
||||
.flattened-pom.xml
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
# Eclipse
|
||||
# ====================
|
||||
# IDE - Eclipse
|
||||
# ====================
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
bin/
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
# ====================
|
||||
# IDE - IntelliJ IDEA
|
||||
# ====================
|
||||
.idea/
|
||||
*.ipr
|
||||
*.iml
|
||||
*.iws
|
||||
out/
|
||||
|
||||
# NetBeans
|
||||
# ====================
|
||||
# IDE - NetBeans
|
||||
# ====================
|
||||
nb-configuration.xml
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
# ====================
|
||||
# IDE - Visual Studio Code
|
||||
# ====================
|
||||
.vscode/
|
||||
.factorypath
|
||||
|
||||
# OSX
|
||||
# ====================
|
||||
# OS - macOS
|
||||
# ====================
|
||||
.DS_Store
|
||||
._*
|
||||
|
||||
# Vim
|
||||
# ====================
|
||||
# OS - Windows
|
||||
# ====================
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
# ====================
|
||||
# Vim / Editors
|
||||
# ====================
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# patch
|
||||
# ====================
|
||||
# Patch files
|
||||
# ====================
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
# Local environment
|
||||
# ====================
|
||||
# Environment & Secrets
|
||||
# ====================
|
||||
.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/
|
||||
.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
4
.mvn/jvm.config
Normal file
@@ -0,0 +1,4 @@
|
||||
-Xmx2048m
|
||||
-Xms1024m
|
||||
-XX:MaxMetaspaceSize=512m
|
||||
-Dfile.encoding=UTF-8
|
||||
93
.mvn/wrapper/MavenWrapperDownloader.java
vendored
93
.mvn/wrapper/MavenWrapperDownloader.java
vendored
@@ -21,77 +21,72 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Authenticator;
|
||||
import java.net.PasswordAuthentication;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public final class MavenWrapperDownloader
|
||||
{
|
||||
private static final String WRAPPER_VERSION = "3.2.0";
|
||||
public final class MavenWrapperDownloader {
|
||||
private static final String WRAPPER_VERSION = "3.3.2";
|
||||
|
||||
private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) );
|
||||
private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE"));
|
||||
|
||||
public static void main( String[] args )
|
||||
{
|
||||
log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION );
|
||||
public static void main(String[] args) {
|
||||
log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION);
|
||||
|
||||
if ( args.length != 2 )
|
||||
{
|
||||
System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" );
|
||||
System.exit( 1 );
|
||||
if (args.length != 2) {
|
||||
System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
log( " - Downloader started" );
|
||||
final URL wrapperUrl = new URL( args[0] );
|
||||
final String jarPath = args[1].replace( "..", "" ); // Sanitize path
|
||||
final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize();
|
||||
downloadFileFromURL( wrapperUrl, wrapperJarPath );
|
||||
log( "Done" );
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
System.err.println( "- Error downloading: " + e.getMessage() );
|
||||
if ( VERBOSE )
|
||||
{
|
||||
try {
|
||||
log(" - Downloader started");
|
||||
final URL wrapperUrl = URI.create(args[0]).toURL();
|
||||
final String jarPath = args[1].replace("..", ""); // Sanitize path
|
||||
final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize();
|
||||
downloadFileFromURL(wrapperUrl, wrapperJarPath);
|
||||
log("Done");
|
||||
} catch (IOException e) {
|
||||
System.err.println("- Error downloading: " + e.getMessage());
|
||||
if (VERBOSE) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.exit( 1 );
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath )
|
||||
throws IOException
|
||||
{
|
||||
log( " - Downloading to: " + wrapperJarPath );
|
||||
if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null )
|
||||
{
|
||||
final String username = System.getenv( "MVNW_USERNAME" );
|
||||
final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray();
|
||||
Authenticator.setDefault( new Authenticator()
|
||||
{
|
||||
private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath)
|
||||
throws IOException {
|
||||
log(" - Downloading to: " + wrapperJarPath);
|
||||
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
|
||||
final String username = System.getenv("MVNW_USERNAME");
|
||||
final char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new PasswordAuthentication( username, password );
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
} );
|
||||
});
|
||||
}
|
||||
try ( InputStream inStream = wrapperUrl.openStream() )
|
||||
{
|
||||
Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING );
|
||||
Path temp = wrapperJarPath
|
||||
.getParent()
|
||||
.resolve(wrapperJarPath.getFileName() + "."
|
||||
+ Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
|
||||
try (InputStream inStream = wrapperUrl.openStream()) {
|
||||
Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} finally {
|
||||
Files.deleteIfExists(temp);
|
||||
}
|
||||
log( " - Downloader complete" );
|
||||
log(" - Downloader complete");
|
||||
}
|
||||
|
||||
private static void log( String msg )
|
||||
{
|
||||
if ( VERBOSE )
|
||||
{
|
||||
System.out.println( msg );
|
||||
private static void log(String msg) {
|
||||
if (VERBOSE) {
|
||||
System.out.println(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
.mvn/wrapper/maven-wrapper.properties
vendored
6
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -14,5 +14,7 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# 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
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=source
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
|
||||
198
AUDIT_INTEGRAL_FRONTEND_BACKEND.md
Normal file
198
AUDIT_INTEGRAL_FRONTEND_BACKEND.md
Normal 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 l’URL/body sans preuve d’identité |
|
||||
| **Couches backend** | Partiel | Resource accède parfois au repository ; validation incohérente (manuel vs Bean Validation) |
|
||||
| **Gestion d’erreurs backend** | Partiel | Réponse d’erreur JSON construite à la main (risque d’injection) ; 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 98–104, 168–174) : appelle directement `usersRepository.findById(userId)` pour vérifier l’existence de l’utilisateur, 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 l’injection 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 d’annotations `@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 l’endpoint 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 l’identité du token. Les paramètres comme `userId` dans l’URL 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 l’appelant 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 d’un autre utilisateur en devinant ou en énumérant des UUID.
|
||||
|
||||
**Recommandation :** Introduire l’authentification 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 62–65) :
|
||||
`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 d’erreur 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 d’exceptions 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.) n’ajoute d’en-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 l’attacher aux requêtes.
|
||||
- L’API backend n’exige aujourd’hui pas de JWT ; en revanche, dès que l’auth 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 d’utiliser `http.Client` brut sans headers d’auth.
|
||||
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 d’attente 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`). C’est cohérent.
|
||||
- Vérifier que partout où l’on parse le body d’erreur, on utilise une clé unique (ex. `error` ou `message`) alignée avec le backend. Après correction du backend (réponse d’erreur 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 l’URL/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 d’erreur | 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 l’autorisation.
|
||||
- **Flutter** : Clean architecture avec repositories abstraits ; couche data qui envoie toujours l’auth (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 **l’authentification et l’autorisation** : côté backend, aucun contrôle sur l’identité de l’appelant ; côté frontend, aucun token n’est 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 d’erreurs (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
305
DATABASE_CONFIG.md
Normal 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
|
||||
600
DEPLOYMENT.md
Normal file
600
DEPLOYMENT.md
Normal file
@@ -0,0 +1,600 @@
|
||||
# 🚀 Guide de Déploiement AfterWork Server
|
||||
|
||||
## 📋 Vue d'Ensemble
|
||||
|
||||
Ce guide décrit le processus de déploiement de l'API AfterWork sur le VPS via `lionsctl pipeline`.
|
||||
|
||||
**URL de l'API** : `https://api.lions.dev/afterwork`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Prérequis
|
||||
|
||||
### Environnement Local
|
||||
- Java 17 (JDK)
|
||||
- Maven 3.9+
|
||||
- Docker 20.10+
|
||||
- `lionsctl` CLI installé et configuré
|
||||
|
||||
### Environnement Serveur
|
||||
- PostgreSQL 15+
|
||||
- Kubernetes cluster configuré
|
||||
- Ingress Controller (nginx)
|
||||
- Cert-Manager pour les certificats SSL (Let's Encrypt)
|
||||
|
||||
---
|
||||
|
||||
## 📁 Fichiers de Configuration
|
||||
|
||||
### 1. Variables d'Environnement Requises
|
||||
|
||||
Les variables suivantes doivent être définies dans Kubernetes Secrets :
|
||||
|
||||
```yaml
|
||||
DB_HOST: postgres # Hostname du serveur PostgreSQL
|
||||
DB_PORT: 5432 # Port PostgreSQL
|
||||
DB_NAME: afterwork_db # Nom de la base de données
|
||||
DB_USERNAME: afterwork # Utilisateur de la base de données
|
||||
DB_PASSWORD: <secret> # Mot de passe (à définir dans le secret)
|
||||
```
|
||||
|
||||
### 2. docker/Dockerfile.prod
|
||||
|
||||
Le fichier `docker/Dockerfile.prod` utilise une approche multi-stage :
|
||||
- **Stage 1** : Build avec Maven dans une image UBI8 OpenJDK 17
|
||||
- **Stage 2** : Runtime optimisé avec l'uber-jar compilé
|
||||
|
||||
### 3. application-prod.properties
|
||||
|
||||
Configuration production avec :
|
||||
- Context path : `/afterwork`
|
||||
- CORS : `https://afterwork.lions.dev`
|
||||
- Health checks : `/q/health/ready` et `/q/health/live`
|
||||
- Métriques : `/q/metrics`
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Build de l'Image Docker
|
||||
|
||||
### Build Local (Test)
|
||||
|
||||
```bash
|
||||
# Build de l'image (Dockerfiles dans docker/)
|
||||
docker build -f docker/Dockerfile.prod -t afterwork-api:latest .
|
||||
|
||||
# Test local
|
||||
docker run -p 8080:8080 \
|
||||
-e DB_HOST=localhost \
|
||||
-e DB_PORT=5432 \
|
||||
-e DB_NAME=afterwork_db \
|
||||
-e DB_USERNAME=afterwork \
|
||||
-e DB_PASSWORD=changeme \
|
||||
afterwork-api:latest
|
||||
```
|
||||
|
||||
### Build pour Registry
|
||||
|
||||
```bash
|
||||
# Tag pour le registry
|
||||
docker tag afterwork-api:latest registry.lions.dev/afterwork-api:1.0.0
|
||||
docker tag afterwork-api:latest registry.lions.dev/afterwork-api:latest
|
||||
|
||||
# Push vers le registry
|
||||
docker push registry.lions.dev/afterwork-api:1.0.0
|
||||
docker push registry.lions.dev/afterwork-api:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚢 Déploiement avec lionsctl pipeline
|
||||
|
||||
La commande **`lionsctl pipeline`** clone le repo Git, compile (Maven), construit l'image Docker, déploie sur Kubernetes et envoie une notification email. Il n'y a pas de sous-commande `deploy` : tout est inclus dans `lionsctl pipeline`.
|
||||
|
||||
### Commande de déploiement
|
||||
|
||||
Remplacez `<org>` par votre organisation Git (ex. `lionsdev`, `developer`) et `<email>` par l'adresse de notification.
|
||||
|
||||
```bash
|
||||
# Déploiement en dev (clone + build + image + déploiement K8s)
|
||||
lionsctl pipeline \
|
||||
-u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
|
||||
-b develop \
|
||||
-j 17 \
|
||||
-e dev \
|
||||
-c k1 \
|
||||
-m <email>
|
||||
|
||||
# Déploiement en production sur le cluster k2
|
||||
lionsctl pipeline \
|
||||
-u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
|
||||
-b main \
|
||||
-j 17 \
|
||||
-e production \
|
||||
-c k2 \
|
||||
-m <email> \
|
||||
-p prod
|
||||
|
||||
# Avec déploiement Helm (charts générés automatiquement)
|
||||
lionsctl pipeline \
|
||||
-u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main \
|
||||
-b develop \
|
||||
-j 17 \
|
||||
-e dev \
|
||||
-c k1 \
|
||||
-m <email> \
|
||||
--use-helm
|
||||
```
|
||||
|
||||
**Options principales :**
|
||||
|
||||
| Option | Description | Exemple |
|
||||
|--------|-------------|---------|
|
||||
| `-u`, `--url` | URL du repo Git (obligatoire) | `https://git.lions.dev/.../mic-after-work-server-impl-quarkus-main` |
|
||||
| `-b`, `--branch` | Branche à déployer | `develop`, `main` |
|
||||
| `-j`, `--java-version` | Version Java (8–21) | `17` |
|
||||
| `-e`, `--environment` | Environnement (dev / staging / production) | `dev`, `production` |
|
||||
| `-c`, `--cluster` | Cluster Kubernetes (k1 ou k2) (obligatoire) | `k1`, `k2` |
|
||||
| `-m`, `--mail` | Email(s) pour les notifications | `admin@lions.dev` |
|
||||
| `-p`, `--profile` | Profil Maven | `prod` pour production |
|
||||
| `--use-helm` | Déployer via Helm | — |
|
||||
|
||||
### Vérification du déploiement
|
||||
|
||||
```bash
|
||||
# Pods et statut (nom d'app dérivé du repo, ex. mic-after-work-server-impl-quarkus-main)
|
||||
kubectl get pods -n applications -l app=mic-after-work-server-impl-quarkus-main
|
||||
|
||||
# Logs en temps réel
|
||||
kubectl logs -n applications -l app=mic-after-work-server-impl-quarkus-main -f
|
||||
|
||||
# Health check
|
||||
curl https://api.lions.dev/afterwork/q/health/ready
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Structure Kubernetes
|
||||
|
||||
### 1. Secret (kubernetes/afterwork-secrets.yaml)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: afterwork-secrets
|
||||
namespace: applications
|
||||
type: Opaque
|
||||
stringData:
|
||||
DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION"
|
||||
```
|
||||
|
||||
### 2. ConfigMap (kubernetes/afterwork-configmap.yaml)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: afterwork-config
|
||||
namespace: applications
|
||||
data:
|
||||
DB_HOST: "postgres"
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: "afterwork_db"
|
||||
DB_USERNAME: "afterwork"
|
||||
QUARKUS_PROFILE: "prod"
|
||||
TZ: "Africa/Douala"
|
||||
```
|
||||
|
||||
### 3. Deployment (kubernetes/afterwork-deployment.yaml)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: afterwork-api
|
||||
namespace: applications
|
||||
labels:
|
||||
app: afterwork-api
|
||||
version: 1.0.0
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: afterwork-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: afterwork-api
|
||||
spec:
|
||||
containers:
|
||||
- name: afterwork-api
|
||||
image: registry.lions.dev/afterwork-api:1.0.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: afterwork-config
|
||||
- secretRef:
|
||||
name: afterwork-secrets
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /afterwork/q/health/live
|
||||
port: 8080
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /afterwork/q/health/ready
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
imagePullSecrets:
|
||||
- name: registry-credentials
|
||||
```
|
||||
|
||||
### 4. Service (kubernetes/afterwork-service.yaml)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: afterwork-api
|
||||
namespace: applications
|
||||
labels:
|
||||
app: afterwork-api
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: afterwork-api
|
||||
```
|
||||
|
||||
### 5. Ingress (kubernetes/afterwork-ingress.yaml)
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: afterwork-api
|
||||
namespace: applications
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /$2
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- api.lions.dev
|
||||
secretName: afterwork-api-tls
|
||||
rules:
|
||||
- host: api.lions.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /afterwork(/|$)(.*)
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: afterwork-api
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Processus de Déploiement Complet
|
||||
|
||||
### Étape 1 : Préparation
|
||||
|
||||
```bash
|
||||
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
||||
|
||||
# Build Maven
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# Vérifier que le JAR est créé
|
||||
ls target/*-runner.jar
|
||||
```
|
||||
|
||||
### Étape 2 : Build Docker
|
||||
|
||||
```bash
|
||||
# Build l'image de production (Dockerfiles dans docker/)
|
||||
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 .
|
||||
|
||||
# Test local (optionnel)
|
||||
docker run --rm -p 8080:8080 \
|
||||
-e DB_HOST=postgres \
|
||||
-e DB_NAME=afterwork_db \
|
||||
-e DB_USERNAME=afterwork \
|
||||
-e DB_PASSWORD=test123 \
|
||||
registry.lions.dev/afterwork-api:1.0.0
|
||||
```
|
||||
|
||||
### Étape 3 : Push vers Registry
|
||||
|
||||
```bash
|
||||
# Login au registry
|
||||
docker login registry.lions.dev
|
||||
|
||||
# Push
|
||||
docker push registry.lions.dev/afterwork-api:1.0.0
|
||||
docker tag registry.lions.dev/afterwork-api:1.0.0 registry.lions.dev/afterwork-api:latest
|
||||
docker push registry.lions.dev/afterwork-api:latest
|
||||
```
|
||||
|
||||
### Étape 4 : Déploiement Kubernetes
|
||||
|
||||
```bash
|
||||
# Créer le namespace si nécessaire
|
||||
kubectl create namespace applications --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Créer les secrets (MODIFIER LES VALEURS!)
|
||||
kubectl apply -f kubernetes/afterwork-secrets.yaml
|
||||
|
||||
# Créer la ConfigMap
|
||||
kubectl apply -f kubernetes/afterwork-configmap.yaml
|
||||
|
||||
# Déployer l'application
|
||||
kubectl apply -f kubernetes/afterwork-deployment.yaml
|
||||
kubectl apply -f kubernetes/afterwork-service.yaml
|
||||
kubectl apply -f kubernetes/afterwork-ingress.yaml
|
||||
|
||||
# Ou via lionsctl pipeline (clone + build + déploiement)
|
||||
lionsctl pipeline -u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m <email>
|
||||
```
|
||||
|
||||
### Étape 5 : Vérification
|
||||
|
||||
```bash
|
||||
# Pods
|
||||
kubectl get pods -n applications -l app=afterwork-api
|
||||
|
||||
# Logs
|
||||
kubectl logs -n applications -l app=afterwork-api --tail=100 -f
|
||||
|
||||
# Service
|
||||
kubectl get svc -n applications afterwork-api
|
||||
|
||||
# Ingress
|
||||
kubectl get ingress -n applications afterwork-api
|
||||
|
||||
# Test health
|
||||
curl https://api.lions.dev/afterwork/q/health/ready
|
||||
curl https://api.lions.dev/afterwork/q/health/live
|
||||
|
||||
# Test API
|
||||
curl https://api.lions.dev/afterwork/api/users/test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Maintenance
|
||||
|
||||
### Mise à Jour de l'Application
|
||||
|
||||
```bash
|
||||
# 1. Build nouvelle version
|
||||
mvn clean package -DskipTests
|
||||
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.1 .
|
||||
docker push registry.lions.dev/afterwork-api:1.0.1
|
||||
|
||||
# 2. Mise à jour du déploiement
|
||||
kubectl set image deployment/afterwork-api \
|
||||
afterwork-api=registry.lions.dev/afterwork-api:1.0.1 \
|
||||
-n applications
|
||||
|
||||
# 3. Rollout status
|
||||
kubectl rollout status deployment/afterwork-api -n applications
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
```bash
|
||||
# Voir l'historique
|
||||
kubectl rollout history deployment/afterwork-api -n applications
|
||||
|
||||
# Rollback à la version précédente
|
||||
kubectl rollout undo deployment/afterwork-api -n applications
|
||||
|
||||
# Rollback à une révision spécifique
|
||||
kubectl rollout undo deployment/afterwork-api --to-revision=2 -n applications
|
||||
```
|
||||
|
||||
### Scaling
|
||||
|
||||
```bash
|
||||
# Scale up
|
||||
kubectl scale deployment afterwork-api --replicas=3 -n applications
|
||||
|
||||
# Scale down
|
||||
kubectl scale deployment afterwork-api --replicas=1 -n applications
|
||||
|
||||
# Autoscaling (HPA)
|
||||
kubectl autoscale deployment afterwork-api \
|
||||
--min=2 --max=10 \
|
||||
--cpu-percent=80 \
|
||||
-n applications
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Problème : Pods ne démarrent pas
|
||||
|
||||
```bash
|
||||
# Vérifier les événements
|
||||
kubectl describe pod <pod-name> -n applications
|
||||
|
||||
# Vérifier les logs
|
||||
kubectl logs <pod-name> -n applications
|
||||
|
||||
# Vérifier les secrets
|
||||
kubectl get secret afterwork-secrets -n applications -o yaml
|
||||
```
|
||||
|
||||
### Problème : Base de données inaccessible
|
||||
|
||||
```bash
|
||||
# Tester la connexion depuis un pod
|
||||
kubectl run -it --rm debug --image=postgres:15 --restart=Never -- \
|
||||
psql -h postgres -U afterwork -d afterwork_db
|
||||
|
||||
# Vérifier le service PostgreSQL
|
||||
kubectl get svc -n postgresql
|
||||
```
|
||||
|
||||
### Problème : Ingress ne fonctionne pas
|
||||
|
||||
```bash
|
||||
# Vérifier l'Ingress
|
||||
kubectl describe ingress afterwork-api -n applications
|
||||
|
||||
# Vérifier les certificats TLS
|
||||
kubectl get certificate -n applications
|
||||
|
||||
# Logs du contrôleur Ingress
|
||||
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### Métriques Prometheus
|
||||
|
||||
```bash
|
||||
# Accéder aux métriques
|
||||
curl https://api.lions.dev/afterwork/q/metrics
|
||||
|
||||
# Ou via port-forward
|
||||
kubectl port-forward -n applications svc/afterwork-api 8080:8080
|
||||
curl http://localhost:8080/q/metrics
|
||||
```
|
||||
|
||||
### Logs Centralisés
|
||||
|
||||
```bash
|
||||
# Tous les logs de l'application
|
||||
kubectl logs -n applications -l app=afterwork-api --tail=1000
|
||||
|
||||
# Logs en temps réel
|
||||
kubectl logs -n applications -l app=afterwork-api -f
|
||||
|
||||
# Logs d'un pod spécifique
|
||||
kubectl logs -n applications <pod-name> --previous
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sécurité
|
||||
|
||||
### Secrets
|
||||
|
||||
- ⚠️ **NE JAMAIS** commiter les secrets dans Git
|
||||
- Utiliser Sealed Secrets ou Vault pour la gestion des secrets
|
||||
- Rotation régulière des mots de passe de base de données
|
||||
|
||||
### Network Policies
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: afterwork-api-netpol
|
||||
namespace: applications
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: afterwork-api
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: ingress-nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: postgresql
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443 # Pour les APIs externes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Checklist de Déploiement
|
||||
|
||||
### Avant le Déploiement
|
||||
|
||||
- [ ] Tests unitaires passent
|
||||
- [ ] Build Maven réussit
|
||||
- [ ] Image Docker créée
|
||||
- [ ] Variables d'environnement configurées
|
||||
- [ ] Secrets créés dans Kubernetes
|
||||
- [ ] Base de données PostgreSQL prête
|
||||
|
||||
### Pendant le Déploiement
|
||||
|
||||
- [ ] Image pushée vers le registry
|
||||
- [ ] Manifests Kubernetes appliqués
|
||||
- [ ] Pods démarrent correctement
|
||||
- [ ] Health checks réussissent
|
||||
- [ ] Ingress configuré avec TLS
|
||||
|
||||
### Après le Déploiement
|
||||
|
||||
- [ ] API accessible via HTTPS
|
||||
- [ ] WebSocket fonctionne
|
||||
- [ ] Tests d'intégration passent
|
||||
- [ ] Métriques remontées dans Prometheus
|
||||
- [ ] Logs centralisés fonctionnent
|
||||
- [ ] Documentation mise à jour
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
En cas de problème :
|
||||
1. Consulter les logs : `kubectl logs -n applications -l app=afterwork-api`
|
||||
2. Vérifier les events : `kubectl get events -n applications`
|
||||
3. Tester les health checks : `curl https://api.lions.dev/afterwork/q/health`
|
||||
4. Contacter l'équipe DevOps
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-01-09
|
||||
**Version** : 1.0.0
|
||||
169
QUICK_DEPLOY.md
Normal file
169
QUICK_DEPLOY.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 🚀 Déploiement Rapide AfterWork API
|
||||
|
||||
## ⚡ Commandes de Déploiement (Copier-Coller)
|
||||
|
||||
### Option 1 : Déploiement Automatique via Script PowerShell
|
||||
|
||||
```powershell
|
||||
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
||||
|
||||
# Déploiement complet (build + push + deploy)
|
||||
.\scripts\deploy.ps1 -Action all -Version 1.0.0
|
||||
|
||||
# Ou étape par étape
|
||||
.\scripts\deploy.ps1 -Action build # Build Maven + Docker
|
||||
.\scripts\deploy.ps1 -Action push # Push vers registry
|
||||
.\scripts\deploy.ps1 -Action deploy # Déploiement K8s
|
||||
|
||||
# Vérifier le statut
|
||||
.\scripts\deploy.ps1 -Action status
|
||||
```
|
||||
|
||||
### Option 2 : Déploiement Manuel
|
||||
|
||||
```powershell
|
||||
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
||||
|
||||
# 1. Build Maven (tests non-bloquants)
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 2. Build Docker
|
||||
docker build -f docker/Dockerfile.prod -t registry.lions.dev/afterwork-api:1.0.0 -t registry.lions.dev/afterwork-api:latest .
|
||||
|
||||
# 3. Push vers Registry
|
||||
docker login registry.lions.dev
|
||||
docker push registry.lions.dev/afterwork-api:1.0.0
|
||||
docker push registry.lions.dev/afterwork-api:latest
|
||||
|
||||
# 4. Déploiement Kubernetes
|
||||
kubectl create namespace applications --dry-run=client -o yaml | kubectl apply -f -
|
||||
kubectl apply -f kubernetes/afterwork-configmap.yaml
|
||||
kubectl apply -f kubernetes/afterwork-secrets.yaml
|
||||
kubectl apply -f kubernetes/afterwork-deployment.yaml
|
||||
kubectl apply -f kubernetes/afterwork-service.yaml
|
||||
kubectl apply -f kubernetes/afterwork-ingress.yaml
|
||||
|
||||
# 5. Vérification
|
||||
kubectl get pods -n applications -l app=afterwork-api
|
||||
kubectl logs -n applications -l app=afterwork-api -f
|
||||
```
|
||||
|
||||
### Option 3 : Déploiement via lionsctl pipeline
|
||||
|
||||
```bash
|
||||
cd C:\Users\dadyo\PersonalProjects\mic-after-work-server-impl-quarkus-main
|
||||
|
||||
# Le pipeline clone le repo, build Maven, construit l’image Docker et déploie sur K8s. Remplacer <org> et <email>.
|
||||
|
||||
# Déploiement
|
||||
lionsctl pipeline -u https://git.lions.dev/<org>/mic-after-work-server-impl-quarkus-main -b develop -j 17 -e dev -c k1 -m <email>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ IMPORTANT : Modifier les Secrets AVANT le Déploiement
|
||||
|
||||
```bash
|
||||
# Éditer le fichier de secrets
|
||||
notepad kubernetes/afterwork-secrets.yaml
|
||||
|
||||
# Changer la ligne:
|
||||
# DB_PASSWORD: "CHANGE_ME_IN_PRODUCTION"
|
||||
# Par le vrai mot de passe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Vérifications Post-Déploiement
|
||||
|
||||
```bash
|
||||
# 1. Pods en cours d'exécution
|
||||
kubectl get pods -n applications -l app=afterwork-api
|
||||
|
||||
# 2. Health check
|
||||
curl https://api.lions.dev/afterwork/q/health/ready
|
||||
curl https://api.lions.dev/afterwork/q/health/live
|
||||
|
||||
# 3. Logs
|
||||
kubectl logs -n applications -l app=afterwork-api --tail=50
|
||||
|
||||
# 4. Ingress
|
||||
kubectl get ingress -n applications afterwork-api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration Frontend
|
||||
|
||||
Une fois l'API déployée, builder l'application Flutter :
|
||||
|
||||
```powershell
|
||||
cd C:\Users\dadyo\PersonalProjects\lions-workspace\afterwork
|
||||
|
||||
# Build APK production
|
||||
.\build-prod.ps1 -Target apk
|
||||
|
||||
# Ou Build AAB pour Play Store
|
||||
.\build-prod.ps1 -Target appbundle
|
||||
|
||||
# Les APKs seront dans:
|
||||
# build/app/outputs/flutter-apk/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting Rapide
|
||||
|
||||
### Si les pods ne démarrent pas :
|
||||
```bash
|
||||
kubectl describe pod <pod-name> -n applications
|
||||
kubectl logs <pod-name> -n applications
|
||||
```
|
||||
|
||||
### Si l'API n'est pas accessible :
|
||||
```bash
|
||||
# Vérifier l'Ingress
|
||||
kubectl describe ingress afterwork-api -n applications
|
||||
|
||||
# Vérifier les certificats TLS
|
||||
kubectl get certificate -n applications
|
||||
|
||||
# Port-forward pour test direct
|
||||
kubectl port-forward -n applications svc/afterwork-api 8080:8080
|
||||
curl http://localhost:8080/afterwork/q/health
|
||||
```
|
||||
|
||||
### Si la base de données est inaccessible :
|
||||
```bash
|
||||
# Tester la connexion DB depuis un pod
|
||||
kubectl run -it --rm debug --image=postgres:15 --restart=Never -- \
|
||||
psql -h postgres -U afterwork -d afterwork_db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checklist Pré-Déploiement
|
||||
|
||||
- [ ] PostgreSQL est installé et accessible sur le cluster
|
||||
- [ ] La base de données `afterwork_db` existe
|
||||
- [ ] L'utilisateur `afterwork` a les droits sur la base
|
||||
- [ ] Le mot de passe DB est configuré dans `kubernetes/afterwork-secrets.yaml`
|
||||
- [ ] Docker est installé et fonctionnel
|
||||
- [ ] Accès au registry `registry.lions.dev` configuré
|
||||
- [ ] kubectl configuré et accès au cluster K8s
|
||||
- [ ] Ingress Controller (nginx) installé sur le cluster
|
||||
- [ ] Cert-Manager installé pour les certificats SSL
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Résumé des URLs
|
||||
|
||||
- **API Production** : `https://api.lions.dev/afterwork`
|
||||
- **Health Check** : `https://api.lions.dev/afterwork/q/health`
|
||||
- **Métriques** : `https://api.lions.dev/afterwork/q/metrics`
|
||||
- **WebSocket** : `wss://api.lions.dev/afterwork/ws/notifications/{userId}`
|
||||
|
||||
---
|
||||
|
||||
**Temps estimé de déploiement** : 5-10 minutes
|
||||
**Dernière mise à jour** : 2026-01-09
|
||||
46
README.md
46
README.md
@@ -1,4 +1,4 @@
|
||||
# mic-after-work
|
||||
# mic-after-work-server-impl-quarkus-main
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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>.
|
||||
|
||||
## 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 d’amitié (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 l’usage en production (userId issu de l’auth).
|
||||
|
||||
### 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 d’emails 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 d’environnement (`MAILER_HOST`, `MAILER_USERNAME`, `MAILER_PASSWORD`, etc.) ; en test le mailer peut être en mode mock.
|
||||
|
||||
### Paiement Wave (établissements)
|
||||
|
||||
- Initiation de paiement (abonnement mensuel/annuel), webhook `POST /webhooks/wave` pour `payment.completed`, `payment.refunded`, `payment.failed`, etc.
|
||||
- Vérification optionnelle de la signature du webhook (header `X-Wave-Signature`, HMAC-SHA256) si `wave.webhook.secret` est configuré. Voir [SECURITY.md](SECURITY.md).
|
||||
|
||||
---
|
||||
|
||||
## Related Guides
|
||||
|
||||
- Hibernate ORM 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
|
||||
- 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 l’app et les dépendances (PostgreSQL, etc.).
|
||||
|
||||
## Provided Code
|
||||
|
||||
@@ -67,7 +104,6 @@ Create your first JPA entity
|
||||
|
||||
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
|
||||
|
||||
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
|
||||
|
||||
|
||||
### RESTEasy JAX-RS
|
||||
|
||||
119
REALTIME_DEV.md
Normal file
119
REALTIME_DEV.md
Normal 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 l’hôte :
|
||||
```bash
|
||||
docker port <container_id_or_name> 9092
|
||||
```
|
||||
- Si rien n’est 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 l’image et les variables à votre setup si vous en utilisez un autre.)
|
||||
|
||||
### Depuis une autre machine / Docker
|
||||
|
||||
- **Quarkus sur l’hôte, Kafka dans Docker** : `localhost:9092` suffit si le port est mappé (`-p 9092:9092`).
|
||||
- **Quarkus dans Docker, Kafka sur l’hôte** : utilisez `host.docker.internal:9092` (Windows/Mac) ou l’IP de l’hô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 d’erreur 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 d’exception 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 l’app 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 l’app 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 d’ami / 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 n’arrive**
|
||||
- Kafka : le broker est-il bien sur le port 9092 ? `KAFKA_BOOTSTRAP_SERVERS` correct ?
|
||||
- WebSocket : l’URL dans l’app est-elle exactement celle du backend (même hôte/port) ?
|
||||
- CORS : pour Flutter web, le backend doit autoriser l’origine de l’app (déjà géré dans la config actuelle si vous n’avez pas changé l’origine).
|
||||
|
||||
## 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 n’est 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.
|
||||
930
REALTIME_IMPLEMENTATION_EXAMPLES.md
Normal file
930
REALTIME_IMPLEMENTATION_EXAMPLES.md
Normal 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
33
SECURITY.md
Normal 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, l’API ne repose pas sur JWT/OAuth pour les endpoints métier. En production, il est recommandé d’ajouter un filtre ou une ressource qui dérive l’identité (userId) du token (JWT/session) et de **ne pas faire confiance au `userId` passé dans l’URL** (ex. `GET /notifications/user/{userId}`). L’`userId` utilisé doit être celui de l’utilisateur authentifié.
|
||||
|
||||
## Endpoints sensibles
|
||||
|
||||
- **Notifications** (`/notifications/user/{userId}`) : En l’état, tout appelant peut demander les notifications d’un autre utilisateur en changeant `userId`. En production, remplacer `userId` par l’identifiant issu du contexte d’authentification (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 d’environnement (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 d’environnement.
|
||||
- **Email (SMTP)** : `MAILER_USERNAME`, `MAILER_PASSWORD` (et optionnellement `MAILER_FROM`, `MAILER_HOST`, etc.) via variables d’environnement.
|
||||
- **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 l’exposition des headers sensibles (CORS, sécurisation des headers).
|
||||
8880
backend_log.txt
Normal file
8880
backend_log.txt
Normal file
File diff suppressed because it is too large
Load Diff
42
docker/Dockerfile
Normal file
42
docker/Dockerfile
Normal 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"]
|
||||
61
docker/Dockerfile.prod
Normal file
61
docker/Dockerfile.prod
Normal file
@@ -0,0 +1,61 @@
|
||||
##
|
||||
## AfterWork Server - Production Dockerfile
|
||||
## Build stage avec Maven + Runtime optimisé avec UBI8 OpenJDK 17
|
||||
##
|
||||
|
||||
# ======================================
|
||||
# STAGE 1: Build de l'application
|
||||
# ======================================
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 AS builder
|
||||
|
||||
USER root
|
||||
|
||||
# Installation de Maven
|
||||
RUN microdnf install -y maven && microdnf clean all
|
||||
|
||||
# Copie des fichiers du projet (context = racine du projet)
|
||||
WORKDIR /build
|
||||
COPY pom.xml .
|
||||
COPY src ./src
|
||||
|
||||
# Build de l'application (skip tests pour accélérer)
|
||||
RUN mvn clean package -DskipTests -Dquarkus.package.type=uber-jar
|
||||
|
||||
# ======================================
|
||||
# STAGE 2: Image de runtime
|
||||
# ======================================
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18
|
||||
|
||||
# Variables d'environnement par défaut
|
||||
ENV LANG='en_US.UTF-8' \
|
||||
LANGUAGE='en_US:en' \
|
||||
TZ='Africa/Douala' \
|
||||
QUARKUS_PROFILE=prod \
|
||||
DB_HOST=postgresql \
|
||||
DB_PORT=5432 \
|
||||
DB_NAME=afterwork_db \
|
||||
DB_USERNAME=afterwork \
|
||||
DB_PASSWORD=changeme \
|
||||
JAVA_OPTS_APPEND="-XX:+UseG1GC \
|
||||
-XX:+StringDeduplication \
|
||||
-XX:+OptimizeStringConcat \
|
||||
-XX:MaxRAMPercentage=75.0 \
|
||||
-XX:+HeapDumpOnOutOfMemoryError \
|
||||
-XX:HeapDumpPath=/tmp/heapdump.hprof \
|
||||
-Djava.net.preferIPv4Stack=true"
|
||||
|
||||
# Configuration du port
|
||||
EXPOSE 8080
|
||||
|
||||
# Copie de l'uber-jar depuis le builder
|
||||
COPY --from=builder --chown=185:185 /build/target/*-runner.jar /deployments/app.jar
|
||||
|
||||
# User non-root pour la sécurité
|
||||
USER 185
|
||||
|
||||
# Healthcheck sur l'endpoint Quarkus
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/q/health/ready || exit 1
|
||||
|
||||
# Lancement de l'application
|
||||
ENTRYPOINT ["java", "-jar", "/deployments/app.jar"]
|
||||
54
docker/README.md
Normal file
54
docker/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Docker AfterWork
|
||||
|
||||
Fichiers Docker pour le build et l’exécution de l’API AfterWork.
|
||||
|
||||
## Fichiers
|
||||
|
||||
| Fichier | Usage |
|
||||
|---------|--------|
|
||||
| `Dockerfile` | Image dev (JAR pré-buildé, Alpine) |
|
||||
| `Dockerfile.prod` | Image prod (multi-stage Maven + UBI8) |
|
||||
| `docker-compose.yml` | Stack optionnelle (app + PostgreSQL/Kafka sur l’hôte) |
|
||||
|
||||
## Build (depuis la racine du projet)
|
||||
|
||||
```bash
|
||||
# Image de production
|
||||
docker build -f docker/Dockerfile.prod -t afterwork-quarkus:latest .
|
||||
|
||||
# Image de dev (après mvn package)
|
||||
docker build -f docker/Dockerfile -t afterwork-quarkus:dev .
|
||||
```
|
||||
|
||||
## Docker Compose
|
||||
|
||||
Utilise le PostgreSQL et Kafka déjà en cours d’exécution sur l’hôte (host.docker.internal).
|
||||
|
||||
**Depuis la racine :**
|
||||
```bash
|
||||
docker-compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
**Depuis docker/ :**
|
||||
```bash
|
||||
cd docker && docker-compose up -d
|
||||
```
|
||||
|
||||
### PostgreSQL (obligatoire)
|
||||
|
||||
L’application se connecte à PostgreSQL sur l’hôte (`host.docker.internal:5432`). Sans identifiants, l’erreur **« no password was provided »** apparaît.
|
||||
|
||||
- **Par défaut** (si vous ne définissez rien) : `DB_USERNAME=afterwork`, `DB_PASSWORD=changeme`, `DB_NAME=afterwork_db`.
|
||||
- Créer la base et l’utilisateur dans PostgreSQL, par exemple :
|
||||
```sql
|
||||
CREATE USER afterwork WITH PASSWORD 'changeme';
|
||||
CREATE DATABASE afterwork_db OWNER afterwork;
|
||||
```
|
||||
- Ou utiliser **vos** identifiants via un fichier **`.env` à la racine du projet** (mic-after-work-server-impl-quarkus-main) — Docker Compose le charge quand vous lancez depuis cette racine :
|
||||
```bash
|
||||
# Contenu de .env à la racine du projet
|
||||
DB_USERNAME=monuser
|
||||
DB_PASSWORD=monmotdepasse
|
||||
DB_NAME=afterwork_db
|
||||
```
|
||||
Si vous lancez depuis `docker/` (`cd docker && docker-compose up`), placez le `.env` dans le dossier `docker/`.
|
||||
26
docker/docker-compose.yml
Normal file
26
docker/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Dev: mvn quarkus:dev (H2 in-memory, Swagger /q/swagger-ui, Kafka localhost:9092)
|
||||
# App utilise le PostgreSQL existant (ex: skyfile sur 5432) - créer la DB: CREATE DATABASE afterwork_db;
|
||||
# Lancer depuis la racine: docker-compose -f docker/docker-compose.yml up -d
|
||||
# Ou depuis docker/: docker-compose up -d
|
||||
#
|
||||
# PostgreSQL: définir DB_USERNAME/DB_PASSWORD si votre instance utilise d'autres identifiants.
|
||||
# Exemple .env à la racine docker/ : DB_USERNAME=monuser DB_PASSWORD=monmotdepasse
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile.prod
|
||||
image: afterwork-quarkus:latest
|
||||
container_name: afterwork-quarkus
|
||||
environment:
|
||||
QUARKUS_PROFILE: prod
|
||||
DB_HOST: host.docker.internal
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: "${DB_NAME:-afterwork_db}"
|
||||
DB_USERNAME: "${DB_USERNAME:-afterwork}"
|
||||
DB_PASSWORD: "${DB_PASSWORD:-changeme}"
|
||||
KAFKA_BOOTSTRAP_SERVERS: host.docker.internal:9092
|
||||
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: unless-stopped
|
||||
2
kubernetes/afterwork-configmap.yaml
Normal file
2
kubernetes/afterwork-configmap.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
# ConfigMap déplacé dans afterwork-secrets.yaml pour cohérence
|
||||
# Voir afterwork-secrets.yaml pour la configuration complète
|
||||
156
kubernetes/afterwork-deployment.yaml
Normal file
156
kubernetes/afterwork-deployment.yaml
Normal file
@@ -0,0 +1,156 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mic-after-work-server-impl-quarkus-main
|
||||
namespace: applications
|
||||
labels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
version: "1.0.0"
|
||||
environment: production
|
||||
component: application
|
||||
project: lions-infrastructure-2025
|
||||
annotations:
|
||||
description: "AfterWork API - Application sociale déployée via lionsctl"
|
||||
lionsctl.lions.dev/deployed-by: "lionsctl"
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
version: "1.0.0"
|
||||
component: application
|
||||
project: lions-infrastructure-2025
|
||||
annotations:
|
||||
# Prometheus scraping - Lions Prometheus auto-découvre via ces annotations
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/afterwork/q/metrics"
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
fsGroup: 1001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
terminationGracePeriodSeconds: 30
|
||||
containers:
|
||||
- name: mic-after-work-server-impl-quarkus-main
|
||||
image: registry.lions.dev/lionsdev/mic-after-work-server-impl-quarkus-main:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
# Variables d'environnement depuis ConfigMap et Secrets
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: afterwork-config
|
||||
- secretRef:
|
||||
name: afterwork-secrets
|
||||
env:
|
||||
# Override explicites pour Quarkus
|
||||
- name: QUARKUS_DATASOURCE_DB_KIND
|
||||
value: "postgresql"
|
||||
- name: QUARKUS_DATASOURCE_USERNAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: afterwork-config
|
||||
key: DB_USERNAME
|
||||
- name: QUARKUS_DATASOURCE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: afterwork-secrets
|
||||
key: DB_PASSWORD
|
||||
- name: QUARKUS_DATASOURCE_JDBC_URL
|
||||
value: "jdbc:postgresql://$(DB_HOST):$(DB_PORT)/$(DB_NAME)"
|
||||
# Kafka - Lions Kafka cluster
|
||||
- name: KAFKA_BOOTSTRAP_SERVERS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: afterwork-config
|
||||
key: KAFKA_BOOTSTRAP_SERVERS
|
||||
# JWT
|
||||
- name: SMALLRYE_JWT_SIGN_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: afterwork-secrets
|
||||
key: JWT_SECRET
|
||||
- name: MP_JWT_VERIFY_ISSUER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: afterwork-config
|
||||
key: JWT_ISSUER
|
||||
# Java options
|
||||
- name: JAVA_OPTS
|
||||
value: "-Xms256m -Xmx512m -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "200m"
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1000m"
|
||||
# Health checks HTTP (utilisent les endpoints SmallRye Health)
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /afterwork/q/health/live
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /afterwork/q/health/ready
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
successThreshold: 1
|
||||
failureThreshold: 3
|
||||
# Startup probe pour éviter les kills pendant le démarrage
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /afterwork/q/health/started
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 30
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: tmp-volume
|
||||
mountPath: /tmp
|
||||
- name: logs-volume
|
||||
mountPath: /app/logs
|
||||
volumes:
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
- name: logs-volume
|
||||
emptyDir: {}
|
||||
imagePullSecrets:
|
||||
- name: lionsregistry-secret
|
||||
restartPolicy: Always
|
||||
68
kubernetes/afterwork-ingress.yaml
Normal file
68
kubernetes/afterwork-ingress.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mic-after-work-server-impl-quarkus-main-ingress
|
||||
namespace: applications
|
||||
labels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
annotations:
|
||||
# SSL/TLS
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
|
||||
|
||||
# Metadata
|
||||
description: "Ingress for afterwork-api application on api.lions.dev/afterwork"
|
||||
kubernetes.io/ingress.class: nginx
|
||||
lionsctl.lions.dev/deployed-by: lionsctl
|
||||
lionsctl.lions.dev/domain: api.lions.dev
|
||||
lionsctl.lions.dev/path: /afterwork
|
||||
|
||||
# Proxy settings
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "on"
|
||||
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
||||
|
||||
# WebSocket support
|
||||
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||
nginx.ingress.kubernetes.io/websocket-services: "mic-after-work-server-impl-quarkus-main-service"
|
||||
|
||||
# Security headers and CORS
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "*"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS, PATCH"
|
||||
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
|
||||
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||
nginx.ingress.kubernetes.io/cors-expose-headers: "Content-Length,Content-Range,Content-Disposition"
|
||||
nginx.ingress.kubernetes.io/cors-max-age: "86400"
|
||||
|
||||
# Compression
|
||||
nginx.ingress.kubernetes.io/enable-compression: "true"
|
||||
nginx.ingress.kubernetes.io/compression-types: "text/plain,text/css,application/json,application/javascript,text/xml,application/xml,application/xml+rss,text/javascript"
|
||||
|
||||
# Rate limiting
|
||||
nginx.ingress.kubernetes.io/rate-limit: "1000"
|
||||
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
|
||||
|
||||
# PAS de rewrite-target : le backend sert sous quarkus.http.root-path=/afterwork,
|
||||
# l'Ingress doit transmettre le chemin complet (/afterwork/...) au service.
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- api.lions.dev
|
||||
secretName: api-lions-dev-tls
|
||||
rules:
|
||||
- host: api.lions.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /afterwork
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: mic-after-work-server-impl-quarkus-main-service
|
||||
port:
|
||||
number: 80
|
||||
408
kubernetes/afterwork-monitoring.yaml
Normal file
408
kubernetes/afterwork-monitoring.yaml
Normal file
@@ -0,0 +1,408 @@
|
||||
# ==============================================================================
|
||||
# AfterWork API - Configuration Monitoring pour Lions Infrastructure
|
||||
# ==============================================================================
|
||||
# Cette configuration intègre l'application avec:
|
||||
# - Prometheus (https://prometheus.lions.dev) - scraping auto via annotations
|
||||
# - Grafana (https://grafana.lions.dev) - dashboard dédié
|
||||
# ==============================================================================
|
||||
|
||||
---
|
||||
# ==============================================================================
|
||||
# ServiceMonitor pour Prometheus Operator (si installé)
|
||||
# ==============================================================================
|
||||
# Note: L'infrastructure Lions utilise le scraping via annotations pod, mais
|
||||
# ce ServiceMonitor peut être utilisé si Prometheus Operator est déployé.
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: afterwork-api-monitor
|
||||
namespace: monitoring
|
||||
labels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
release: prometheus
|
||||
project: lions-infrastructure-2025
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- applications
|
||||
endpoints:
|
||||
- port: http-direct
|
||||
path: /afterwork/q/metrics
|
||||
interval: 30s
|
||||
scrapeTimeout: 10s
|
||||
scheme: http
|
||||
|
||||
---
|
||||
# ==============================================================================
|
||||
# PrometheusRule - Alertes pour AfterWork API
|
||||
# ==============================================================================
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: afterwork-api-alerts
|
||||
namespace: monitoring
|
||||
labels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
release: prometheus
|
||||
project: lions-infrastructure-2025
|
||||
spec:
|
||||
groups:
|
||||
- name: afterwork-api.rules
|
||||
rules:
|
||||
# Alerte si l'application est down
|
||||
- alert: AfterWorkAPIDown
|
||||
expr: up{job=~".*afterwork.*"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
application: afterwork-api
|
||||
annotations:
|
||||
summary: "AfterWork API is down"
|
||||
description: "L'API AfterWork n'est pas accessible depuis plus de 2 minutes"
|
||||
|
||||
# Alerte si le taux d'erreur HTTP 5xx est élevé
|
||||
- alert: AfterWorkHighErrorRate
|
||||
expr: |
|
||||
sum(rate(http_server_requests_seconds_count{
|
||||
kubernetes_namespace="applications",
|
||||
app="mic-after-work-server-impl-quarkus-main",
|
||||
status=~"5.."
|
||||
}[5m])) /
|
||||
sum(rate(http_server_requests_seconds_count{
|
||||
kubernetes_namespace="applications",
|
||||
app="mic-after-work-server-impl-quarkus-main"
|
||||
}[5m])) > 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
application: afterwork-api
|
||||
annotations:
|
||||
summary: "High error rate on AfterWork API"
|
||||
description: "Le taux d'erreur 5xx est supérieur à 5% depuis 5 minutes"
|
||||
|
||||
# Alerte si la latence p95 est élevée
|
||||
- alert: AfterWorkHighLatency
|
||||
expr: |
|
||||
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{
|
||||
kubernetes_namespace="applications",
|
||||
app="mic-after-work-server-impl-quarkus-main"
|
||||
}[5m])) by (le)) > 2
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
application: afterwork-api
|
||||
annotations:
|
||||
summary: "High latency on AfterWork API"
|
||||
description: "La latence p95 dépasse 2 secondes depuis 5 minutes"
|
||||
|
||||
# Alerte si la mémoire est proche de la limite
|
||||
- alert: AfterWorkHighMemoryUsage
|
||||
expr: |
|
||||
sum(container_memory_working_set_bytes{
|
||||
namespace="applications",
|
||||
pod=~"mic-after-work-server-impl-quarkus-main.*"
|
||||
}) /
|
||||
sum(container_spec_memory_limit_bytes{
|
||||
namespace="applications",
|
||||
pod=~"mic-after-work-server-impl-quarkus-main.*"
|
||||
}) > 0.85
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
application: afterwork-api
|
||||
annotations:
|
||||
summary: "High memory usage on AfterWork API"
|
||||
description: "L'utilisation mémoire dépasse 85% de la limite"
|
||||
|
||||
# Alerte si le pod redémarre fréquemment
|
||||
- alert: AfterWorkPodRestarts
|
||||
expr: |
|
||||
increase(kube_pod_container_status_restarts_total{
|
||||
namespace="applications",
|
||||
pod=~"mic-after-work-server-impl-quarkus-main.*"
|
||||
}[1h]) > 3
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
application: afterwork-api
|
||||
annotations:
|
||||
summary: "AfterWork API pod restarting frequently"
|
||||
description: "Le pod a redémarré plus de 3 fois dans la dernière heure"
|
||||
|
||||
---
|
||||
# ==============================================================================
|
||||
# Grafana Dashboard ConfigMap (pour import automatique)
|
||||
# ==============================================================================
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: afterwork-grafana-dashboard
|
||||
namespace: monitoring
|
||||
labels:
|
||||
grafana_dashboard: "1"
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
project: lions-infrastructure-2025
|
||||
data:
|
||||
afterwork-api-dashboard.json: |
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "green", "value": null},
|
||||
{"color": "yellow", "value": 100},
|
||||
{"color": "red", "value": 500}
|
||||
]
|
||||
},
|
||||
"unit": "reqps"
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
|
||||
"id": 1,
|
||||
"options": {},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m]))",
|
||||
"legendFormat": "Requests/s",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Request Rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"unit": "ms"
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
||||
"id": 2,
|
||||
"options": {},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) by (le)) * 1000",
|
||||
"legendFormat": "p95 Latency",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.50, sum(rate(http_server_requests_seconds_bucket{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) by (le)) * 1000",
|
||||
"legendFormat": "p50 Latency",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Response Time",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"unit": "percent"
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
|
||||
"id": 3,
|
||||
"options": {},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{kubernetes_namespace=\"applications\",app=\"mic-after-work-server-impl-quarkus-main\"}[5m])) * 100",
|
||||
"legendFormat": "Error Rate %",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Error Rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"unit": "bytes"
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 8},
|
||||
"id": 4,
|
||||
"options": {},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(container_memory_working_set_bytes{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"})",
|
||||
"legendFormat": "Memory Used",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "sum(container_spec_memory_limit_bytes{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"})",
|
||||
"legendFormat": "Memory Limit",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Memory Usage",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"unit": "short"
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16},
|
||||
"id": 5,
|
||||
"options": {},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"}[5m])) * 1000",
|
||||
"legendFormat": "CPU Usage (millicores)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "CPU Usage",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "green", "value": 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 16},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "up{job=~\".*afterwork.*\"}",
|
||||
"legendFormat": "Status",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "API Status",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "green", "value": null},
|
||||
{"color": "yellow", "value": 1},
|
||||
{"color": "red", "value": 3}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 16},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "increase(kube_pod_container_status_restarts_total{namespace=\"applications\",pod=~\"mic-after-work-server-impl-quarkus-main.*\"}[1h])",
|
||||
"legendFormat": "Restarts (1h)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Pod Restarts (1h)",
|
||||
"type": "stat"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": ["lions", "afterwork", "quarkus", "api"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "AfterWork API Dashboard",
|
||||
"uid": "afterwork-api",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
180
kubernetes/afterwork-secrets.yaml
Normal file
180
kubernetes/afterwork-secrets.yaml
Normal file
@@ -0,0 +1,180 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: afterwork-secrets
|
||||
namespace: applications
|
||||
labels:
|
||||
app: afterwork-api
|
||||
component: secrets
|
||||
environment: production
|
||||
project: lions-infrastructure-2025
|
||||
type: Opaque
|
||||
stringData:
|
||||
# ==============================================================================
|
||||
# BASE DE DONNÉES PostgreSQL
|
||||
# ==============================================================================
|
||||
# Utilise le PostgreSQL de l'infrastructure Lions
|
||||
# postgresql-service.postgresql.svc.cluster.local:5432
|
||||
DB_PASSWORD: "AfterWork2025!"
|
||||
|
||||
# ==============================================================================
|
||||
# JWT / SÉCURITÉ
|
||||
# ==============================================================================
|
||||
# Clé secrète JWT (minimum 32 caractères, aléatoire)
|
||||
# Générer avec: openssl rand -base64 32
|
||||
JWT_SECRET: "AfterWorkJWTSecret2025LionsInfrastructureKey"
|
||||
|
||||
# ==============================================================================
|
||||
# COMPTE ADMINISTRATEUR INITIAL
|
||||
# ==============================================================================
|
||||
ADMIN_EMAIL: "admin@afterwork.ci"
|
||||
ADMIN_PASSWORD: "AdminAfterWork2025!"
|
||||
|
||||
# ==============================================================================
|
||||
# SERVICE EMAIL (SMTP)
|
||||
# ==============================================================================
|
||||
# Configuration Gmail ou autre SMTP
|
||||
MAILER_USERNAME: "noreply@afterwork.ci"
|
||||
MAILER_PASSWORD: "CHANGEZ_MOI_SMTP_PASSWORD"
|
||||
|
||||
# ==============================================================================
|
||||
# WAVE PAYMENT (Intégration paiement)
|
||||
# ==============================================================================
|
||||
WAVE_API_KEY: "CHANGEZ_MOI_WAVE_API_KEY"
|
||||
WAVE_SECRET: "CHANGEZ_MOI_WAVE_SECRET"
|
||||
|
||||
---
|
||||
# ==============================================================================
|
||||
# CONFIGMAP POUR CONFIGURATION NON-SENSIBLE
|
||||
# ==============================================================================
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: afterwork-config
|
||||
namespace: applications
|
||||
labels:
|
||||
app: afterwork-api
|
||||
component: configuration
|
||||
environment: production
|
||||
project: lions-infrastructure-2025
|
||||
data:
|
||||
# ==============================================================================
|
||||
# BASE DE DONNÉES - Lions PostgreSQL
|
||||
# ==============================================================================
|
||||
DB_HOST: "postgresql-service.postgresql.svc.cluster.local"
|
||||
DB_PORT: "5432"
|
||||
DB_NAME: "mic-after-work-server-impl-quarkus-main"
|
||||
DB_USERNAME: "lionsuser"
|
||||
|
||||
# ==============================================================================
|
||||
# QUARKUS
|
||||
# ==============================================================================
|
||||
QUARKUS_PROFILE: "prod"
|
||||
QUARKUS_LOG_LEVEL: "INFO"
|
||||
QUARKUS_LOG_CONSOLE_JSON: "true"
|
||||
|
||||
# ==============================================================================
|
||||
# JWT
|
||||
# ==============================================================================
|
||||
JWT_LIFESPAN: "86400"
|
||||
JWT_ISSUER: "afterwork-api"
|
||||
|
||||
# ==============================================================================
|
||||
# KAFKA - Lions Infrastructure
|
||||
# ==============================================================================
|
||||
# Utilise le Kafka déployé dans le namespace kafka
|
||||
KAFKA_BOOTSTRAP_SERVERS: "kafka-service.kafka.svc.cluster.local:9092"
|
||||
|
||||
# ==============================================================================
|
||||
# EMAIL (SMTP)
|
||||
# ==============================================================================
|
||||
MAILER_HOST: "smtp.gmail.com"
|
||||
MAILER_PORT: "587"
|
||||
MAILER_FROM: "AfterWork <noreply@afterwork.ci>"
|
||||
MAILER_START_TLS: "REQUIRED"
|
||||
# En production, mettre false. true = mock (pas d'envoi réel)
|
||||
MAILER_MOCK: "true"
|
||||
|
||||
# ==============================================================================
|
||||
# RATE LIMITING
|
||||
# ==============================================================================
|
||||
AFTERWORK_RATELIMIT_MAX_REQUESTS: "10"
|
||||
AFTERWORK_RATELIMIT_WINDOW_SECONDS: "60"
|
||||
|
||||
# ==============================================================================
|
||||
# WAVE PAYMENT
|
||||
# ==============================================================================
|
||||
WAVE_BASE_URL: "https://api.wave.com"
|
||||
WAVE_CURRENCY: "XOF"
|
||||
WAVE_CALLBACK_URL: "https://api.lions.dev/afterwork/webhooks/wave"
|
||||
|
||||
# ==============================================================================
|
||||
# OBSERVABILITY - Lions Prometheus/Grafana
|
||||
# ==============================================================================
|
||||
# Prometheus scrape via annotations sur le pod
|
||||
# Grafana disponible sur https://grafana.lions.dev
|
||||
|
||||
# ==============================================================================
|
||||
# KEYCLOAK / SSO (optionnel)
|
||||
# ==============================================================================
|
||||
# OIDC_AUTH_SERVER_URL: "https://security.lions.dev/realms/lions"
|
||||
# OIDC_CLIENT_ID: "afterwork-api"
|
||||
|
||||
---
|
||||
# ==============================================================================
|
||||
# EXTERNAL SECRET - Intégration Vault (ACTIF)
|
||||
# ==============================================================================
|
||||
# Vault est déverrouillé sur https://vault.lions.dev
|
||||
# Les secrets sont synchronisés depuis Vault vers Kubernetes automatiquement
|
||||
#
|
||||
# PRÉREQUIS: Créer les secrets dans Vault avec:
|
||||
# vault kv put lions/afterwork \
|
||||
# db_password="AfterWork2025!" \
|
||||
# jwt_secret="AfterWorkJWTSecret2025LionsInfrastructureKey" \
|
||||
# admin_password="AdminAfterWork2025!" \
|
||||
# mailer_password="SMTP_PASSWORD" \
|
||||
# wave_api_key="WAVE_KEY" \
|
||||
# wave_secret="WAVE_SECRET"
|
||||
#
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: afterwork-vault-secrets
|
||||
namespace: applications
|
||||
labels:
|
||||
app: afterwork-api
|
||||
component: external-secrets
|
||||
project: lions-infrastructure-2025
|
||||
spec:
|
||||
refreshInterval: "1h"
|
||||
secretStoreRef:
|
||||
name: vault-backend
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: afterwork-secrets-vault
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: DB_PASSWORD
|
||||
remoteRef:
|
||||
key: lions/data/afterwork
|
||||
property: db_password
|
||||
- secretKey: JWT_SECRET
|
||||
remoteRef:
|
||||
key: lions/data/afterwork
|
||||
property: jwt_secret
|
||||
- secretKey: ADMIN_PASSWORD
|
||||
remoteRef:
|
||||
key: lions/data/afterwork
|
||||
property: admin_password
|
||||
- secretKey: MAILER_PASSWORD
|
||||
remoteRef:
|
||||
key: lions/data/afterwork
|
||||
property: mailer_password
|
||||
- secretKey: WAVE_API_KEY
|
||||
remoteRef:
|
||||
key: lions/data/afterwork
|
||||
property: wave_api_key
|
||||
- secretKey: WAVE_SECRET
|
||||
remoteRef:
|
||||
key: lions/data/afterwork
|
||||
property: wave_secret
|
||||
30
kubernetes/afterwork-service.yaml
Normal file
30
kubernetes/afterwork-service.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mic-after-work-server-impl-quarkus-main-service
|
||||
namespace: applications
|
||||
labels:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
component: application
|
||||
project: lions-infrastructure-2025
|
||||
annotations:
|
||||
description: "Service for AfterWork API"
|
||||
spec:
|
||||
type: ClusterIP
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 10800
|
||||
ports:
|
||||
# Port 80 exposé, route vers 8080 du container
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
# Port 8080 pour compatibilité directe
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http-direct
|
||||
selector:
|
||||
app: mic-after-work-server-impl-quarkus-main
|
||||
256
mvnw
vendored
256
mvnw
vendored
@@ -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:
|
||||
# ------------------
|
||||
@@ -33,75 +33,84 @@
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
if [ -z "$MAVEN_SKIP_RC" ]; then
|
||||
|
||||
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||
if [ -f /usr/local/etc/mavenrc ]; then
|
||||
. /usr/local/etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
if [ -f /etc/mavenrc ]; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
if [ -f "$HOME/.mavenrc" ]; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
cygwin=false
|
||||
darwin=false
|
||||
mingw=false
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true ;;
|
||||
Darwin*)
|
||||
darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"
|
||||
export JAVA_HOME
|
||||
else
|
||||
JAVA_HOME="/Library/Java/Home"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
;;
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -r /etc/gentoo-release ]; then
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] \
|
||||
&& JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] \
|
||||
&& CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
if $mingw; then
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
|
||||
&& JAVA_HOME="$(
|
||||
cd "$JAVA_HOME" || (
|
||||
echo "cannot cd into $JAVA_HOME." >&2
|
||||
exit 1
|
||||
)
|
||||
pwd
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
if $darwin; then
|
||||
javaHome="$(dirname "$javaExecutable")"
|
||||
javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
javaExecutable="$(readlink -f "$javaExecutable")"
|
||||
fi
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome="$(dirname "$javaExecutable")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
@@ -109,52 +118,60 @@ if [ -z "$JAVA_HOME" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
if [ -z "$JAVACMD" ]; then
|
||||
if [ -n "$JAVA_HOME" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
JAVACMD="$(
|
||||
\unset -f command 2>/dev/null
|
||||
\command -v java
|
||||
)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
if [ ! -x "$JAVACMD" ]; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set." >&2
|
||||
fi
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
if [ -z "$1" ]; then
|
||||
echo "Path not specified to find_maven_basedir" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
while [ "$wdir" != '/' ]; do
|
||||
if [ -d "$wdir"/.mvn ]; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
wdir=$(
|
||||
cd "$wdir/.." || exit 1
|
||||
pwd
|
||||
)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
printf '%s' "$(
|
||||
cd "$basedir" || exit 1
|
||||
pwd
|
||||
)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
@@ -165,7 +182,7 @@ concat_lines() {
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
tr -s '\r\n' ' ' <"$1"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -177,10 +194,11 @@ log() {
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
@@ -189,63 +207,66 @@ log "$MAVEN_PROJECTBASEDIR"
|
||||
##########################################################################################
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
|
||||
else
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
|
||||
fi
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in wrapperUrl)
|
||||
wrapperUrl="$safeValue"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget >/dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
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
|
||||
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"
|
||||
|
||||
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
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
else
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
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
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
@@ -254,22 +275,25 @@ fi
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
case "$key" in wrapperSha256Sum)
|
||||
wrapperSha256Sum=$value
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
|
||||
if command -v sha256sum >/dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
if $cygwin; then
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
[ -n "$JAVA_HOME" ] \
|
||||
&& JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] \
|
||||
&& CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] \
|
||||
&& MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
|
||||
21
mvnw.cmd
vendored
21
mvnw.cmd
vendored
@@ -18,7 +18,7 @@
|
||||
@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 Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@@ -59,22 +59,22 @@ set ERROR_CODE=0
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo. >&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 location of your Java installation. >&2
|
||||
echo.
|
||||
echo. >&2
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo. >&2
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
echo. >&2
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
@@ -119,7 +119,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.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 (
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
@@ -133,7 +133,7 @@ if exist %WRAPPER_JAR% (
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.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" (
|
||||
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%=="" (
|
||||
powershell -Command "&{"^
|
||||
"Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
|
||||
413
pom.xml
413
pom.xml
@@ -1,185 +1,244 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.lions.dev</groupId>
|
||||
<artifactId>mic-after-work-server</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>dev.lions</groupId>
|
||||
<artifactId>mic-after-work-server-impl-quarkus-main</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||
<quarkus.platform.version>3.13.0</quarkus.platform.version>
|
||||
<skipITs>true</skipITs>
|
||||
<surefire-plugin.version>3.2.5</surefire-plugin.version>
|
||||
</properties>
|
||||
<properties>
|
||||
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||
<quarkus.platform.version>3.16.3</quarkus.platform.version>
|
||||
<quarkus.package.type>fast-jar</quarkus.package.type>
|
||||
<skipITs>true</skipITs>
|
||||
<surefire-plugin.version>3.5.0</surefire-plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<!-- JWT : émission au login et validation sur les requêtes -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-jwt</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-jwt-build</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-logging-json</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.groovy</groupId>
|
||||
<artifactId>quarkus-groovy-junit5</artifactId>
|
||||
<version>3.16.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<!-- WebSockets Next (remplace quarkus-websockets) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets-next</artifactId>
|
||||
</dependency>
|
||||
<!-- Kafka Reactive Messaging -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-messaging-kafka</artifactId>
|
||||
</dependency>
|
||||
<!-- JSON Serialization pour Kafka -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jsonb</artifactId>
|
||||
</dependency>
|
||||
<!-- Flyway pour les migrations SQL automatiques -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-flyway</artifactId>
|
||||
</dependency>
|
||||
<!-- Scheduler pour jobs planifiés (nettoyage stories, tokens, rappels événements) -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-scheduler</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>at.favre.lib</groupId>
|
||||
<artifactId>bcrypt</artifactId>
|
||||
<version>0.10.2</version>
|
||||
</dependency>
|
||||
<!-- Email Service pour réinitialisation de mot de passe -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-mailer</artifactId>
|
||||
</dependency>
|
||||
<!-- ============================================== -->
|
||||
<!-- HEALTH CHECKS & OBSERVABILITY -->
|
||||
<!-- ============================================== -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-health</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-micrometer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ============================================== -->
|
||||
<!-- TEST DEPENDENCIES -->
|
||||
<!-- ============================================== -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5-mockito</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-test-security</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Jakarta Bean Validation -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.el</groupId>
|
||||
<artifactId>jakarta.el-api</artifactId>
|
||||
<version>5.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
<version>3.13.0</version> <!-- Utilise la même version de Quarkus -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-jwt</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<version>6.3.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.groovy</groupId>
|
||||
<artifactId>quarkus-groovy-junit5</artifactId>
|
||||
<version>3.16.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-oracle</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
<goal>native-image-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
<!-- Ne pas bloquer le build si les tests échouent -->
|
||||
<testFailureIgnore>true</testFailureIgnore>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.34</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
<goal>native-image-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
|
||||
</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>native</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<skipITs>false</skipITs>
|
||||
<quarkus.native.enabled>true</quarkus.native.enabled>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>native</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<skipITs>false</skipITs>
|
||||
<quarkus.native.enabled>true</quarkus.native.enabled>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
20
scripts/README.md
Normal file
20
scripts/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Scripts AfterWork
|
||||
|
||||
Scripts de déploiement et d’outillage.
|
||||
|
||||
## deploy.ps1
|
||||
|
||||
Script PowerShell de déploiement production (build Maven, build Docker, push registry, déploiement Kubernetes).
|
||||
|
||||
**Exécution** (depuis la racine du projet) :
|
||||
|
||||
```powershell
|
||||
.\scripts\deploy.ps1 -Action all -Version 1.0.0
|
||||
.\scripts\deploy.ps1 -Action build # Build Maven + Docker
|
||||
.\scripts\deploy.ps1 -Action push # Push vers registry
|
||||
.\scripts\deploy.ps1 -Action deploy # Déploiement K8s
|
||||
.\scripts\deploy.ps1 -Action status # Statut du déploiement
|
||||
.\scripts\deploy.ps1 -Action rollback # Rollback
|
||||
```
|
||||
|
||||
Le script détecte automatiquement la racine du projet (parent de `scripts/`).
|
||||
309
scripts/deploy.ps1
Normal file
309
scripts/deploy.ps1
Normal file
@@ -0,0 +1,309 @@
|
||||
# ====================================================================
|
||||
# AfterWork Server - Script de Déploiement Production
|
||||
# ====================================================================
|
||||
# Ce script automatise le processus de build et déploiement
|
||||
# de l'API AfterWork sur le VPS via Kubernetes.
|
||||
# Exécuter depuis la racine du projet ou depuis scripts/
|
||||
# ====================================================================
|
||||
|
||||
param(
|
||||
[ValidateSet("build", "push", "deploy", "all", "rollback", "status")]
|
||||
[string]$Action = "all",
|
||||
|
||||
[string]$Version = "1.0.0",
|
||||
|
||||
[string]$Registry = "registry.lions.dev",
|
||||
|
||||
[switch]$SkipTests,
|
||||
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Racine du projet (parent du dossier scripts)
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
|
||||
|
||||
# Couleurs
|
||||
function Write-Info { param($msg) Write-Host $msg -ForegroundColor Cyan }
|
||||
function Write-Success { param($msg) Write-Host $msg -ForegroundColor Green }
|
||||
function Write-Warning { param($msg) Write-Host $msg -ForegroundColor Yellow }
|
||||
function Write-Error { param($msg) Write-Host $msg -ForegroundColor Red }
|
||||
|
||||
# Variables
|
||||
$AppName = "afterwork-api"
|
||||
$Namespace = "applications"
|
||||
$ImageName = "$Registry/${AppName}:$Version"
|
||||
$ImageLatest = "$Registry/${AppName}:latest"
|
||||
|
||||
Write-Info "======================================================================"
|
||||
Write-Info " AfterWork Server - Déploiement Production"
|
||||
Write-Info "======================================================================"
|
||||
Write-Host ""
|
||||
Write-Info "Configuration:"
|
||||
Write-Host " - Action: $Action"
|
||||
Write-Host " - Version: $Version"
|
||||
Write-Host " - Registry: $Registry"
|
||||
Write-Host " - Image: $ImageName"
|
||||
Write-Host " - Namespace: $Namespace"
|
||||
Write-Host " - Racine projet: $ProjectRoot"
|
||||
Write-Host ""
|
||||
|
||||
# ======================================================================
|
||||
# Build Maven
|
||||
# ======================================================================
|
||||
function Build-Application {
|
||||
Write-Info "[1/5] Build Maven..."
|
||||
|
||||
Push-Location $ProjectRoot
|
||||
try {
|
||||
$mavenArgs = "clean", "package", "-Dquarkus.package.type=uber-jar"
|
||||
if ($SkipTests) {
|
||||
$mavenArgs += "-DskipTests"
|
||||
} else {
|
||||
$mavenArgs += "-DtestFailureIgnore=true"
|
||||
}
|
||||
|
||||
& mvn $mavenArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors du build Maven"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$jar = Get-ChildItem -Path (Join-Path $ProjectRoot "target") -Filter "*-runner.jar" | Select-Object -First 1
|
||||
if (-not $jar) {
|
||||
Write-Error "JAR runner non trouvé dans target/"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Build Maven réussi : $($jar.Name)"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Build Docker Image
|
||||
# ======================================================================
|
||||
function Build-DockerImage {
|
||||
Write-Info "[2/5] Build Docker Image..."
|
||||
|
||||
Push-Location $ProjectRoot
|
||||
try {
|
||||
$dockerDir = Join-Path $ProjectRoot "docker"
|
||||
docker build -f (Join-Path $dockerDir "Dockerfile.prod") -t $ImageName -t $ImageLatest .
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors du build Docker"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Image Docker créée : $ImageName"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Push vers Registry
|
||||
# ======================================================================
|
||||
function Push-ToRegistry {
|
||||
Write-Info "[3/5] Push vers Registry..."
|
||||
|
||||
$loginTest = docker login $Registry 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -and -not $loginTest.ToString().Contains("Succeeded")) {
|
||||
Write-Warning "Connexion au registry nécessaire..."
|
||||
docker login $Registry
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Échec de connexion au registry"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
docker push $ImageName
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors du push de $ImageName"
|
||||
exit 1
|
||||
}
|
||||
|
||||
docker push $ImageLatest
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors du push de $ImageLatest"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Images pushées vers $Registry"
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Déploiement Kubernetes
|
||||
# ======================================================================
|
||||
function Deploy-ToKubernetes {
|
||||
Write-Info "[4/5] Déploiement Kubernetes..."
|
||||
|
||||
$kubectlCheck = kubectl version --client 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "kubectl n'est pas installé ou configuré"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$k8sDir = Join-Path $ProjectRoot "kubernetes"
|
||||
Write-Info "Création du namespace $Namespace..."
|
||||
kubectl create namespace $Namespace --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
Write-Info "Application des ConfigMaps et Secrets..."
|
||||
kubectl apply -f (Join-Path $k8sDir "afterwork-configmap.yaml")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "ConfigMap déjà existante ou erreur"
|
||||
}
|
||||
|
||||
if (-not $Force) {
|
||||
Write-Warning "⚠️ ATTENTION : Vérifiez que les secrets sont correctement configurés !"
|
||||
Write-Warning " Fichier : kubernetes/afterwork-secrets.yaml"
|
||||
$confirm = Read-Host "Continuer le déploiement? (o/N)"
|
||||
if ($confirm -ne "o" -and $confirm -ne "O") {
|
||||
Write-Warning "Déploiement annulé"
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
kubectl apply -f (Join-Path $k8sDir "afterwork-secrets.yaml")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors de l'application des secrets"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Info "Déploiement de l'application..."
|
||||
kubectl apply -f (Join-Path $k8sDir "afterwork-deployment.yaml")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors du déploiement"
|
||||
exit 1
|
||||
}
|
||||
|
||||
kubectl apply -f (Join-Path $k8sDir "afterwork-service.yaml")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors de la création du service"
|
||||
exit 1
|
||||
}
|
||||
|
||||
kubectl apply -f (Join-Path $k8sDir "afterwork-ingress.yaml")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors de la création de l'ingress"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Déploiement Kubernetes réussi"
|
||||
|
||||
Write-Info "Attente du rollout..."
|
||||
kubectl rollout status deployment/$AppName -n $Namespace --timeout=5m
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "Timeout ou erreur lors du rollout"
|
||||
}
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Vérification du déploiement
|
||||
# ======================================================================
|
||||
function Verify-Deployment {
|
||||
Write-Info "[5/5] Vérification du déploiement..."
|
||||
|
||||
Write-Info "Pods:"
|
||||
kubectl get pods -n $Namespace -l app=$AppName
|
||||
|
||||
Write-Info "`nService:"
|
||||
kubectl get svc -n $Namespace $AppName
|
||||
|
||||
Write-Info "`nIngress:"
|
||||
kubectl get ingress -n $Namespace $AppName
|
||||
|
||||
Write-Info "`nTest Health Check..."
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "https://api.lions.dev/afterwork/q/health/ready" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Success "✓ API accessible : https://api.lions.dev/afterwork"
|
||||
Write-Success "✓ Health check : OK"
|
||||
} else {
|
||||
Write-Warning "⚠ Health check retourné : $($response.StatusCode)"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "⚠ Impossible de joindre l'API (normal si DNS pas encore propagé)"
|
||||
Write-Info " Vérifiez manuellement : https://api.lions.dev/afterwork/q/health"
|
||||
}
|
||||
|
||||
Write-Success "`nDéploiement terminé avec succès !"
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Rollback
|
||||
# ======================================================================
|
||||
function Rollback-Deployment {
|
||||
Write-Warning "Rollback du déploiement..."
|
||||
|
||||
kubectl rollout undo deployment/$AppName -n $Namespace
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Erreur lors du rollback"
|
||||
exit 1
|
||||
}
|
||||
|
||||
kubectl rollout status deployment/$AppName -n $Namespace
|
||||
Write-Success "Rollback réussi"
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Status
|
||||
# ======================================================================
|
||||
function Get-Status {
|
||||
Write-Info "Status de $AppName..."
|
||||
|
||||
Write-Info "`nPods:"
|
||||
kubectl get pods -n $Namespace -l app=$AppName
|
||||
|
||||
Write-Info "`nDéploiement:"
|
||||
kubectl get deployment -n $Namespace $AppName
|
||||
|
||||
Write-Info "`nService:"
|
||||
kubectl get svc -n $Namespace $AppName
|
||||
|
||||
Write-Info "`nIngress:"
|
||||
kubectl get ingress -n $Namespace $AppName
|
||||
|
||||
Write-Info "`nLogs récents (20 dernières lignes):"
|
||||
kubectl logs -n $Namespace -l app=$AppName --tail=20
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# Exécution selon l'action
|
||||
# ======================================================================
|
||||
|
||||
switch ($Action) {
|
||||
"build" {
|
||||
Build-Application
|
||||
Build-DockerImage
|
||||
}
|
||||
"push" {
|
||||
Push-ToRegistry
|
||||
}
|
||||
"deploy" {
|
||||
Deploy-ToKubernetes
|
||||
Verify-Deployment
|
||||
}
|
||||
"all" {
|
||||
Build-Application
|
||||
Build-DockerImage
|
||||
Push-ToRegistry
|
||||
Deploy-ToKubernetes
|
||||
Verify-Deployment
|
||||
}
|
||||
"rollback" {
|
||||
Rollback-Deployment
|
||||
}
|
||||
"status" {
|
||||
Get-Status
|
||||
}
|
||||
}
|
||||
|
||||
Write-Info "`n======================================================================"
|
||||
Write-Info "Terminé!"
|
||||
Write-Info "======================================================================"
|
||||
@@ -7,11 +7,11 @@
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# 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
|
||||
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
|
||||
@@ -20,7 +20,7 @@
|
||||
#
|
||||
# Then run the container using :
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-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 scripts computes the command line to execute your Java application, and
|
||||
@@ -77,7 +77,7 @@
|
||||
# accessed directly. (example: "foo.example.com,bar.example.com")
|
||||
#
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.19
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.20
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# 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
|
||||
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
|
||||
@@ -20,7 +20,7 @@
|
||||
#
|
||||
# Then run the container using :
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/mic-after-work-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 scripts computes the command line to execute your Java application, and
|
||||
@@ -77,7 +77,7 @@
|
||||
# accessed directly. (example: "foo.example.com,bar.example.com")
|
||||
#
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.19
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.20
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# 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/
|
||||
RUN chown 1001 /work \
|
||||
&& chmod "g+rwX" /work \
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# 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
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Service pour la base de données PostgreSQL
|
||||
db:
|
||||
image: postgres:13
|
||||
container_name: afterwork_db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- afterwork-network
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
|
||||
# Service pour l'application Quarkus
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/main/docker/Dockerfile.jvm
|
||||
container_name: afterwork-quarkus
|
||||
environment:
|
||||
DB_USERNAME: ${DB_USERNAME}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DB_NAME: ${DB_NAME}
|
||||
JAVA_OPTS_APPEND: "-Dquarkus.http.host=0.0.0.0"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- afterwork-network
|
||||
|
||||
# Service pour Swagger UI
|
||||
swagger-ui:
|
||||
image: swaggerapi/swagger-ui
|
||||
container_name: afterwork-swagger-ui
|
||||
environment:
|
||||
SWAGGER_JSON: http://app:8080/openapi
|
||||
ports:
|
||||
- "8081:8080"
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- afterwork-network
|
||||
|
||||
networks:
|
||||
afterwork-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
driver: local
|
||||
44
src/main/java/com/lions/dev/config/OpenAPIConfig.java
Normal file
44
src/main/java/com/lions/dev/config/OpenAPIConfig.java
Normal 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
|
||||
}
|
||||
171
src/main/java/com/lions/dev/config/ScheduledJobs.java
Normal file
171
src/main/java/com/lions/dev/config/ScheduledJobs.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/lions/dev/config/SuperAdminStartup.java
Normal file
59
src/main/java/com/lions/dev/config/SuperAdminStartup.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.lions.dev.config;
|
||||
|
||||
import com.lions.dev.entity.users.Users;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import com.lions.dev.util.UserRoles;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Crée le super administrateur au démarrage de l'application si aucun n'existe.
|
||||
* Email et mot de passe configurables (variables d'environnement en production).
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class SuperAdminStartup {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SuperAdminStartup.class);
|
||||
|
||||
@Inject
|
||||
UsersRepository usersRepository;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.email", defaultValue = "superadmin@afterwork.lions.dev")
|
||||
String superAdminEmail;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.password", defaultValue = "SuperAdmin2025!")
|
||||
String superAdminPassword;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.first-name", defaultValue = "Super")
|
||||
String superAdminFirstName;
|
||||
|
||||
@ConfigProperty(name = "afterwork.super-admin.last-name", defaultValue = "Administrator")
|
||||
String superAdminLastName;
|
||||
|
||||
@Transactional
|
||||
void onStart(@Observes StartupEvent event) {
|
||||
if (usersRepository.findByEmail(superAdminEmail).isPresent()) {
|
||||
LOG.info("Super administrateur déjà présent (email: " + superAdminEmail + "). Aucune création.");
|
||||
return;
|
||||
}
|
||||
|
||||
Users superAdmin = new Users();
|
||||
superAdmin.setFirstName(superAdminFirstName);
|
||||
superAdmin.setLastName(superAdminLastName);
|
||||
superAdmin.setEmail(superAdminEmail);
|
||||
superAdmin.setPassword(superAdminPassword);
|
||||
superAdmin.setRole(UserRoles.SUPER_ADMIN);
|
||||
superAdmin.setProfileImageUrl("https://placehold.co/150x150.png");
|
||||
superAdmin.setBio("Super administrateur AfterWork");
|
||||
superAdmin.setLoyaltyPoints(0);
|
||||
superAdmin.setVerified(true);
|
||||
|
||||
usersRepository.persist(superAdmin);
|
||||
LOG.info("Super administrateur créé au démarrage : " + superAdminEmail + " (role: " + UserRoles.SUPER_ADMIN + ")");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ public class Failures {
|
||||
*/
|
||||
public Failures(String failureMessage) {
|
||||
this.failureMessage = failureMessage;
|
||||
System.out.println("[FAILURE] Échec détecté : " + failureMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
package com.lions.dev.core.errors;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.lions.dev.core.errors.exceptions.BadRequestException;
|
||||
import com.lions.dev.core.errors.exceptions.EventNotFoundException;
|
||||
import com.lions.dev.core.errors.exceptions.NotFoundException;
|
||||
import com.lions.dev.core.errors.exceptions.ServerException;
|
||||
import com.lions.dev.core.errors.exceptions.UnauthorizedException;
|
||||
import com.lions.dev.exception.EstablishmentHasDependenciesException;
|
||||
import com.lions.dev.exception.FriendshipNotFoundException;
|
||||
import com.lions.dev.exception.UserNotFoundException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Gestionnaire global des exceptions pour l'API.
|
||||
* Ce gestionnaire intercepte les exceptions spécifiques et renvoie des réponses appropriées.
|
||||
* Les réponses d'erreur sont sérialisées en JSON de façon sûre (pas de concaténation de chaînes).
|
||||
*/
|
||||
@Provider
|
||||
public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GlobalExceptionHandler.class);
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Gère les exceptions non traitées et retourne une réponse appropriée.
|
||||
*
|
||||
* @param exception L'exception interceptée.
|
||||
* @return Une réponse HTTP avec un message d'erreur et le code de statut approprié.
|
||||
*/
|
||||
@Override
|
||||
public Response toResponse(Throwable exception) {
|
||||
if (exception instanceof BadRequestException) {
|
||||
logger.warn("BadRequestException intercepted: " + exception.getMessage());
|
||||
return buildResponse(Response.Status.BAD_REQUEST, exception.getMessage());
|
||||
} else if (exception instanceof UserNotFoundException) {
|
||||
logger.warn("UserNotFoundException (404): " + exception.getMessage());
|
||||
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
|
||||
} else if (exception instanceof FriendshipNotFoundException) {
|
||||
logger.warn("FriendshipNotFoundException (404): " + exception.getMessage());
|
||||
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
|
||||
} else if (exception instanceof EstablishmentHasDependenciesException) {
|
||||
logger.warn("EstablishmentHasDependenciesException (409): " + exception.getMessage());
|
||||
return buildResponse(Response.Status.CONFLICT, exception.getMessage());
|
||||
} else if (exception instanceof EventNotFoundException || exception instanceof NotFoundException) {
|
||||
logger.warn("NotFoundException intercepted: " + exception.getMessage());
|
||||
return buildResponse(Response.Status.NOT_FOUND, exception.getMessage());
|
||||
@@ -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.
|
||||
*
|
||||
* @param status Le code de statut HTTP.
|
||||
* @param message Le message d'erreur.
|
||||
* @return La réponse HTTP formée.
|
||||
* Le message est sérialisé en JSON de façon sûre (échappement automatique).
|
||||
*/
|
||||
private Response buildResponse(Response.Status status, String message) {
|
||||
return Response.status(status)
|
||||
.entity("{\"error\":\"" + message + "\"}")
|
||||
.build();
|
||||
Map<String, String> body = Collections.singletonMap("error", message != null ? message : "");
|
||||
try {
|
||||
return Response.status(status)
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.entity(OBJECT_MAPPER.writeValueAsString(body))
|
||||
.build();
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Impossible de sérialiser la réponse d'erreur", e);
|
||||
return Response.status(status).type(MediaType.APPLICATION_JSON).entity("{\"error\":\"Erreur serveur\"}").build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
package com.lions.dev.core.errors;
|
||||
|
||||
public class ServerException {
|
||||
}
|
||||
@@ -16,7 +16,6 @@ public class BadRequestException extends WebApplicationException {
|
||||
*/
|
||||
public BadRequestException(String message) {
|
||||
super(message, Response.Status.BAD_REQUEST);
|
||||
System.out.println("[ERROR] Requête invalide : " + message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@ public class NotFoundException extends WebApplicationException {
|
||||
*/
|
||||
public NotFoundException(String message) {
|
||||
super(message, Response.Status.NOT_FOUND);
|
||||
System.out.println("[ERROR] Ressource non trouvée : " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,5 @@ public class ServerException extends RuntimeException {
|
||||
*/
|
||||
public ServerException(String message) {
|
||||
super(message);
|
||||
System.out.println("[ERROR] Erreur serveur : " + message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@ public class UnauthorizedException extends WebApplicationException {
|
||||
*/
|
||||
public UnauthorizedException(String message) {
|
||||
super(message, Response.Status.UNAUTHORIZED);
|
||||
System.out.println("[ERROR] Accès non autorisé : " + message);
|
||||
}
|
||||
}
|
||||
|
||||
83
src/main/java/com/lions/dev/core/security/JwtAuthFilter.java
Normal file
83
src/main/java/com/lions/dev/core/security/JwtAuthFilter.java
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/lions/dev/core/security/RequiresAuth.java
Normal file
33
src/main/java/com/lions/dev/core/security/RequiresAuth.java
Normal 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;
|
||||
}
|
||||
19
src/main/java/com/lions/dev/dto/PasswordResetRequest.java
Normal file
19
src/main/java/com/lions/dev/dto/PasswordResetRequest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
75
src/main/java/com/lions/dev/dto/events/ChatMessageEvent.java
Normal file
75
src/main/java/com/lions/dev/dto/events/ChatMessageEvent.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
48
src/main/java/com/lions/dev/dto/events/PresenceEvent.java
Normal file
48
src/main/java/com/lions/dev/dto/events/PresenceEvent.java
Normal 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();
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/lions/dev/dto/events/ReactionEvent.java
Normal file
63
src/main/java/com/lions/dev/dto/events/ReactionEvent.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.lions.dev.dto.request.booking;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO de création de réservation (aligné frontend).
|
||||
*/
|
||||
public class ReservationCreateRequestDTO {
|
||||
|
||||
@NotNull(message = "userId est obligatoire")
|
||||
private UUID userId;
|
||||
|
||||
@NotNull(message = "establishmentId est obligatoire")
|
||||
private UUID establishmentId;
|
||||
|
||||
/** Date/heure de réservation (ISO-8601 ou timestamp). */
|
||||
private String reservationDate;
|
||||
|
||||
@Min(1)
|
||||
private int numberOfPeople = 1;
|
||||
|
||||
private String notes;
|
||||
|
||||
public UUID getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(UUID userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public UUID getEstablishmentId() {
|
||||
return establishmentId;
|
||||
}
|
||||
|
||||
public void setEstablishmentId(UUID establishmentId) {
|
||||
this.establishmentId = establishmentId;
|
||||
}
|
||||
|
||||
public String getReservationDate() {
|
||||
return reservationDate;
|
||||
}
|
||||
|
||||
public void setReservationDate(String reservationDate) {
|
||||
this.reservationDate = reservationDate;
|
||||
}
|
||||
|
||||
public int getNumberOfPeople() {
|
||||
return numberOfPeople;
|
||||
}
|
||||
|
||||
public void setNumberOfPeople(int numberOfPeople) {
|
||||
this.numberOfPeople = numberOfPeople;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -6,9 +6,14 @@ import lombok.Setter;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* DTO pour la création d'un événement.
|
||||
*
|
||||
* Version 2.0 - Architecture refactorée avec nommage standardisé.
|
||||
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
|
||||
*
|
||||
* Ce DTO est utilisé dans les requêtes de création d'événements, envoyant les informations
|
||||
* nécessaires comme le titre, les dates, la description, le créateur, et d'autres attributs.
|
||||
*/
|
||||
@@ -28,15 +33,44 @@ public class EventCreateRequestDTO {
|
||||
@NotNull(message = "La date de fin est obligatoire.")
|
||||
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 link; // Lien d'information supplémentaire
|
||||
private String imageUrl; // URL de l'image associée à l'événement
|
||||
|
||||
private Integer maxParticipants; // Nombre maximum de participants autorisés
|
||||
private String tags; // Tags/mots-clés associés à l'événement (séparés par des virgules)
|
||||
private String organizer; // Nom de l'organisateur de l'événement
|
||||
private Integer participationFee; // Frais de participation en centimes
|
||||
private Boolean isPrivate = false; // v2.0 - Indique si l'événement est privé
|
||||
private Boolean waitlistEnabled = false; // v2.0 - Indique si la liste d'attente est activée
|
||||
private String privacyRules; // Règles de confidentialité de l'événement
|
||||
private String transportInfo; // Informations sur les transports disponibles
|
||||
private String accommodationInfo; // Informations sur l'hébergement
|
||||
private String accessibilityInfo; // Informations sur l'accessibilité
|
||||
private String parkingInfo; // Informations sur le parking
|
||||
private String securityProtocol; // Protocole de sécurité de l'événement
|
||||
|
||||
@NotNull(message = "L'identifiant du créateur est obligatoire.")
|
||||
private UUID creatorId; // Identifiant du créateur de l'événement
|
||||
|
||||
// Champ déprécié (v1.0) - conservé pour compatibilité mais ignoré
|
||||
/**
|
||||
* @deprecated Supprimé en v2.0 (utiliser establishmentId à la place).
|
||||
*/
|
||||
@Deprecated
|
||||
private String location;
|
||||
|
||||
public EventCreateRequestDTO() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.lions.dev.dto.request.events;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* DTO pour la suppression d'un événement.
|
||||
@@ -15,6 +16,5 @@ public class EventDeleteRequestDTO {
|
||||
private UUID eventId; // ID de l'événement à supprimer
|
||||
|
||||
public EventDeleteRequestDTO() {
|
||||
System.out.println("[LOG] DTO de requête de suppression d'événement initialisé.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ import lombok.Setter;
|
||||
@AllArgsConstructor
|
||||
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.
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.lions.dev.dto.request.events;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* DTO pour lire un événement par son ID.
|
||||
@@ -15,6 +16,5 @@ public class EventReadOneByIdRequestDTO {
|
||||
private UUID eventId; // ID de l'événement à lire
|
||||
|
||||
public EventReadOneByIdRequestDTO() {
|
||||
System.out.println("[LOG] DTO de requête de lecture d'événement initialisé.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.lions.dev.dto.request.friends;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -15,7 +16,10 @@ import java.util.UUID;
|
||||
@NoArgsConstructor
|
||||
public class FriendshipCreateOneRequestDTO {
|
||||
|
||||
@NotNull(message = "L'identifiant de l'utilisateur est requis")
|
||||
private UUID userId; // ID de l'utilisateur qui envoie la demande
|
||||
|
||||
@NotNull(message = "L'identifiant de l'ami est requis")
|
||||
private UUID friendId; // ID de l'utilisateur qui reçoit la demande
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lions.dev.dto.request.users;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* DTO pour l'attribution d'un rôle à un utilisateur (opération réservée au super admin).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class AssignRoleRequestDTO {
|
||||
|
||||
private static final String ROLE_PATTERN = "SUPER_ADMIN|ADMIN|MANAGER|USER";
|
||||
|
||||
@NotBlank(message = "Le rôle est obligatoire")
|
||||
@Pattern(regexp = ROLE_PATTERN, message = "Rôle invalide. Valeurs autorisées : SUPER_ADMIN, ADMIN, MANAGER, USER")
|
||||
private String role;
|
||||
|
||||
public AssignRoleRequestDTO(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DTO pour la requête d'authentification de l'utilisateur.
|
||||
*
|
||||
* Version 2.0 - Architecture refactorée avec nommage standardisé.
|
||||
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
|
||||
*
|
||||
* Utilisé pour encapsuler les informations nécessaires lors de l'authentification d'un utilisateur.
|
||||
*/
|
||||
@Getter
|
||||
@@ -25,9 +29,16 @@ public class UserAuthenticateRequestDTO {
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* Mot de passe de l'utilisateur en texte clair.
|
||||
* Ce champ sera haché avant d'être utilisé pour l'authentification.
|
||||
* Mot de passe hashé de l'utilisateur (v2.0).
|
||||
* 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;
|
||||
|
||||
/**
|
||||
@@ -37,6 +48,15 @@ public class UserAuthenticateRequestDTO {
|
||||
logger.info("UserAuthenticateRequestDTO - DTO pour l'authentification initialisé");
|
||||
}
|
||||
|
||||
/**
|
||||
* Méthode pour obtenir le mot de passe (compatibilité v1.0 et v2.0).
|
||||
*
|
||||
* @return Le mot de passe (password_hash ou motDePasse).
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password_hash != null ? password_hash : motDePasse;
|
||||
}
|
||||
|
||||
// Méthode personnalisée pour loguer les détails de la requête
|
||||
public void logRequestDetails() {
|
||||
logger.info("Authentification demandée pour l'email: {}", email);
|
||||
|
||||
@@ -7,21 +7,25 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* DTO pour la création et l'authentification d'un utilisateur.
|
||||
* Ce DTO est utilisé dans les requêtes pour créer ou authentifier un utilisateur,
|
||||
* contenant les informations comme le nom, les prénoms, l'email, et le mot de passe.
|
||||
* DTO pour la création d'un utilisateur.
|
||||
*
|
||||
* Version 2.0 - Architecture refactorée avec nommage standardisé.
|
||||
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
|
||||
*
|
||||
* Ce DTO est utilisé dans les requêtes pour créer un utilisateur,
|
||||
* contenant les informations comme le prénom, le nom, l'email, et le mot de passe.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class UserCreateRequestDTO {
|
||||
|
||||
@NotNull(message = "Le nom est obligatoire.")
|
||||
@Size(min = 1, max = 100, message = "Le nom doit comporter entre 1 et 100 caractères.")
|
||||
private String nom;
|
||||
@NotNull(message = "Le prénom est obligatoire.")
|
||||
@Size(min = 1, max = 100, message = "Le prénom doit comporter entre 1 et 100 caractères.")
|
||||
private String firstName; // v2.0
|
||||
|
||||
@NotNull(message = "Les prénoms sont obligatoires.")
|
||||
@Size(min = 1, max = 100, message = "Les prénoms doivent comporter entre 1 et 100 caractères.")
|
||||
private String prenoms;
|
||||
@NotNull(message = "Le nom de famille est obligatoire.")
|
||||
@Size(min = 1, max = 100, message = "Le nom de famille doit comporter entre 1 et 100 caractères.")
|
||||
private String lastName; // v2.0
|
||||
|
||||
@NotNull(message = "L'adresse email est obligatoire.")
|
||||
@Email(message = "Veuillez fournir une adresse email valide.")
|
||||
@@ -29,11 +33,86 @@ public class UserCreateRequestDTO {
|
||||
|
||||
@NotNull(message = "Le mot de passe est obligatoire.")
|
||||
@Size(min = 6, message = "Le mot de passe doit comporter au moins 6 caractères.")
|
||||
private String motDePasse;
|
||||
private String password; // v2.0 - sera hashé en passwordHash
|
||||
|
||||
private String profileImageUrl;
|
||||
|
||||
private String bio; // v2.0
|
||||
|
||||
private Integer loyaltyPoints = 0; // v2.0
|
||||
|
||||
/**
|
||||
* Préférences utilisateur (v2.0).
|
||||
*
|
||||
* Structure attendue:
|
||||
* {
|
||||
* "preferredCategory": "RESTAURANT" | "BAR" | "CLUB" | "CAFE" | "EVENT" | null,
|
||||
* "notifications": {
|
||||
* "email": boolean,
|
||||
* "push": boolean
|
||||
* },
|
||||
* "language": "fr" | "en" | "es"
|
||||
* }
|
||||
*
|
||||
* Exemple:
|
||||
* {
|
||||
* "preferredCategory": "RESTAURANT",
|
||||
* "notifications": {
|
||||
* "email": true,
|
||||
* "push": true
|
||||
* },
|
||||
* "language": "fr"
|
||||
* }
|
||||
*/
|
||||
private java.util.Map<String, Object> preferences; // v2.0
|
||||
|
||||
// Ajout du rôle avec validation
|
||||
@NotNull(message = "Le rôle est obligatoire.")
|
||||
private String role; // Rôle de l'utilisateur (par exemple : ADMIN, USER, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.lions.dev.dto.response.admin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminRevenueResponseDTO {
|
||||
|
||||
/** Revenus totaux (abonnements actifs * prix). */
|
||||
private BigDecimal totalRevenueXof;
|
||||
/** Nombre d'abonnements actifs. */
|
||||
private long activeSubscriptionsCount;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lions.dev.dto.response.admin;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ManagerStatsResponseDTO {
|
||||
|
||||
private UUID userId;
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
/** ACTIVE ou SUSPENDED (isActive). */
|
||||
private String status;
|
||||
private LocalDateTime subscriptionExpiresAt;
|
||||
private UUID establishmentId;
|
||||
private String establishmentName;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.lions.dev.dto.response.booking;
|
||||
|
||||
import com.lions.dev.entity.booking.Booking;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* DTO de réponse pour une réservation (aligné sur le frontend Flutter ReservationModel).
|
||||
* eventId/eventTitle : pour les réservations d'établissement, eventTitle = nom de l'établissement, eventId = null.
|
||||
*/
|
||||
@Getter
|
||||
public class ReservationResponseDTO {
|
||||
|
||||
private final String id;
|
||||
private final String userId;
|
||||
private final String userFullName;
|
||||
private final String eventId; // null pour résa établissement
|
||||
private final String eventTitle; // nom événement ou établissement
|
||||
private final String reservationDate; // ISO-8601
|
||||
private final int numberOfPeople;
|
||||
private final String status; // PENDING, CONFIRMED, CANCELLED, COMPLETED
|
||||
private final String establishmentId;
|
||||
private final String establishmentName;
|
||||
private final String notes;
|
||||
private final String createdAt; // ISO-8601
|
||||
|
||||
public ReservationResponseDTO(Booking booking) {
|
||||
this.id = booking.getId() != null ? booking.getId().toString() : null;
|
||||
this.userId = booking.getUser() != null && booking.getUser().getId() != null
|
||||
? booking.getUser().getId().toString() : null;
|
||||
this.userFullName = booking.getUser() != null
|
||||
? (booking.getUser().getFirstName() + " " + booking.getUser().getLastName()).trim()
|
||||
: "";
|
||||
this.eventId = null; // Réservation établissement sans événement
|
||||
this.eventTitle = booking.getEstablishment() != null ? booking.getEstablishment().getName() : "";
|
||||
this.reservationDate = booking.getReservationTime() != null
|
||||
? booking.getReservationTime().toString() : null;
|
||||
this.numberOfPeople = booking.getGuestCount() != null ? booking.getGuestCount() : 1;
|
||||
this.status = booking.getStatus() != null ? booking.getStatus().toLowerCase() : "pending";
|
||||
this.establishmentId = booking.getEstablishment() != null && booking.getEstablishment().getId() != null
|
||||
? booking.getEstablishment().getId().toString() : null;
|
||||
this.establishmentName = booking.getEstablishment() != null ? booking.getEstablishment().getName() : null;
|
||||
this.notes = booking.getSpecialRequests();
|
||||
this.createdAt = booking.getCreatedAt() != null ? booking.getCreatedAt().toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,9 @@ public class CommentResponseDTO {
|
||||
this.id = comment.getId(); // Identifiant unique du commentaire
|
||||
this.texte = comment.getText(); // Texte du commentaire
|
||||
this.userId = comment.getUser().getId(); // Identifiant de l'utilisateur (auteur du commentaire)
|
||||
this.userNom = comment.getUser().getNom(); // Nom de l'utilisateur
|
||||
this.userPrenoms = comment.getUser().getPrenoms(); // Prénom de l'utilisateur
|
||||
// v2.0 - Utiliser les nouveaux noms de champs
|
||||
this.userNom = comment.getUser().getLastName(); // Nom de famille de l'utilisateur (v2.0)
|
||||
this.userPrenoms = comment.getUser().getFirstName(); // Prénom de l'utilisateur (v2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.lions.dev.dto.response.events;
|
||||
|
||||
import com.lions.dev.entity.events.Events;
|
||||
import com.lions.dev.repository.UsersRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour renvoyer les informations d'un événement.
|
||||
*
|
||||
* Version 2.0 - Architecture refactorée avec nommage standardisé.
|
||||
* Conforme à l'architecture de données AfterWork v2.0 (Ultra-Compétitive).
|
||||
*
|
||||
* Ce DTO est utilisé pour structurer les données retournées dans les réponses
|
||||
* après les opérations sur les événements (création, récupération).
|
||||
*/
|
||||
@@ -16,33 +22,95 @@ public class EventCreateResponseDTO {
|
||||
private String description; // Description de l'événement
|
||||
private LocalDateTime startDate; // Date de début de l'événement
|
||||
private LocalDateTime endDate; // Date de fin de l'événement
|
||||
private String 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 link; // Lien vers plus d'informations
|
||||
private String imageUrl; // URL d'une image pour l'événement
|
||||
private String creatorId; // ID du créateur de l'événement
|
||||
private String creatorEmail; // Email du créateur de l'événement
|
||||
private String creatorFirstName; // Prénom du créateur de l'événement
|
||||
private String creatorLastName; // Nom de famille du création de l'événement
|
||||
private String status; // Statut de l'événement
|
||||
private String creatorFirstName; // v2.0 - Prénom du créateur de l'événement
|
||||
private String creatorLastName; // v2.0 - Nom de famille du créateur de l'événement
|
||||
private String status; // Statut de l'événement (OPEN, CLOSED, CANCELLED, COMPLETED)
|
||||
private Boolean isPrivate; // v2.0 - Indique si l'événement est privé
|
||||
private Boolean waitlistEnabled; // v2.0 - Indique si la liste d'attente est activée
|
||||
private Integer maxParticipants; // Nombre maximum de participants autorisés
|
||||
private Integer participationFee; // Frais de participation en centimes
|
||||
private Integer participantsCount; // ✅ Nombre actuel de participants (event.getParticipants().size())
|
||||
private Integer commentsCount; // ✅ Nombre de commentaires (event.getComments().size())
|
||||
private Integer sharesCount; // ✅ Nombre de partages (event.getShares().size())
|
||||
private Long reactionsCount; // ✅ Nombre de réactions (utilisateurs qui ont cet événement en favori)
|
||||
private Boolean isFavorite; // ✅ Indique si l'utilisateur actuel a cet événement en favori (optionnel, dépend du contexte)
|
||||
|
||||
// Champ déprécié (v1.0) - conservé pour compatibilité
|
||||
/**
|
||||
* @deprecated Utiliser {@link #establishmentId} et {@link #establishmentName} à la place.
|
||||
*/
|
||||
@Deprecated
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 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 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.title = event.getTitle();
|
||||
this.description = event.getDescription();
|
||||
this.startDate = event.getStartDate();
|
||||
this.endDate = event.getEndDate();
|
||||
this.location = event.getLocation();
|
||||
this.category = event.getCategory();
|
||||
this.link = event.getLink();
|
||||
this.imageUrl = event.getImageUrl();
|
||||
this.creatorEmail = event.getCreator().getEmail();
|
||||
this.creatorFirstName = event.getCreator().getPrenoms();
|
||||
this.creatorLastName = event.getCreator().getNom();
|
||||
this.status = event.getStatus();
|
||||
this.isPrivate = event.getIsPrivate(); // v2.0
|
||||
this.waitlistEnabled = event.getWaitlistEnabled(); // v2.0
|
||||
this.maxParticipants = event.getMaxParticipants();
|
||||
this.participationFee = event.getParticipationFee();
|
||||
this.participantsCount = event.getParticipants() != null ? event.getParticipants().size() : 0;
|
||||
this.commentsCount = event.getComments() != null ? event.getComments().size() : 0;
|
||||
this.sharesCount = event.getShares() != null ? event.getShares().size() : 0;
|
||||
|
||||
// ✅ Calculer reactionsCount si usersRepository est fourni
|
||||
if (usersRepository != null) {
|
||||
this.reactionsCount = usersRepository.countUsersWithFavoriteEvent(event.getId());
|
||||
} else {
|
||||
this.reactionsCount = 0L;
|
||||
}
|
||||
|
||||
// ✅ Vérifier isFavorite si currentUserId est fourni
|
||||
if (currentUserId != null && usersRepository != null) {
|
||||
this.isFavorite = usersRepository.hasUserFavoriteEvent(currentUserId, event.getId());
|
||||
} else {
|
||||
this.isFavorite = null;
|
||||
}
|
||||
|
||||
// v2.0 - Informations sur l'établissement
|
||||
if (event.getEstablishment() != null) {
|
||||
this.establishmentId = event.getEstablishment().getId().toString();
|
||||
this.establishmentName = event.getEstablishment().getName();
|
||||
this.location = event.getLocation(); // Méthode qui retourne l'adresse de l'établissement
|
||||
}
|
||||
|
||||
// v2.0 - Informations sur le créateur
|
||||
if (event.getCreator() != null) {
|
||||
this.creatorId = event.getCreator().getId().toString();
|
||||
this.creatorEmail = event.getCreator().getEmail();
|
||||
this.creatorFirstName = event.getCreator().getFirstName(); // v2.0
|
||||
this.creatorLastName = event.getCreator().getLastName(); // v2.0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur simplifié sans calcul de réactions (pour compatibilité).
|
||||
*
|
||||
* @param event L'événement à convertir en DTO.
|
||||
*/
|
||||
public EventCreateResponseDTO(Events event) {
|
||||
this(event, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class EventReadManyByIdResponseDTO {
|
||||
private String profileImageUrl; // URL de l'image de profil de l'utilisateur qui a criané l'événement
|
||||
|
||||
/**
|
||||
* Constructeur qui transforme une entité Events en DTO de réponse.
|
||||
* Constructeur qui transforme une entité Events en DTO de réponse (v2.0).
|
||||
*
|
||||
* @param event L'événement à convertir en DTO.
|
||||
*/
|
||||
@@ -37,14 +37,16 @@ public class EventReadManyByIdResponseDTO {
|
||||
this.description = event.getDescription();
|
||||
this.startDate = event.getStartDate();
|
||||
this.endDate = event.getEndDate();
|
||||
// v2.0 - Utiliser getLocation() qui retourne l'adresse de l'établissement
|
||||
this.location = event.getLocation();
|
||||
this.category = event.getCategory();
|
||||
this.link = event.getLink();
|
||||
this.imageUrl = event.getImageUrl();
|
||||
this.status = event.getStatus();
|
||||
this.creatorEmail = event.getCreator().getEmail();
|
||||
this.creatorFirstName = event.getCreator().getPrenoms();
|
||||
this.creatorLastName = event.getCreator().getNom();
|
||||
// v2.0 - Utiliser les nouveaux noms de champs
|
||||
this.creatorFirstName = event.getCreator().getFirstName();
|
||||
this.creatorLastName = event.getCreator().getLastName();
|
||||
this.profileImageUrl = event.getCreator().getProfileImageUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,24 @@ public class FriendshipCreateOneResponseDTO {
|
||||
|
||||
/**
|
||||
* Constructeur pour mapper l'entité `Friendship` à ce DTO.
|
||||
* Utilise les IDs fournis pour éviter LazyInitializationException sur user/friend.
|
||||
*
|
||||
* @param friendship L'entité `Friendship` à convertir en DTO.
|
||||
* @param userId ID de l'utilisateur qui envoie la demande (déjà chargé).
|
||||
* @param friendId ID de l'utilisateur qui reçoit la demande (déjà chargé).
|
||||
*/
|
||||
public FriendshipCreateOneResponseDTO(Friendship friendship, UUID userId, UUID friendId) {
|
||||
this.id = friendship.getId();
|
||||
this.userId = userId;
|
||||
this.friendId = friendId;
|
||||
this.status = friendship.getStatus();
|
||||
this.createdAt = friendship.getCreatedAt();
|
||||
this.updatedAt = friendship.getUpdatedAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructeur pour mapper l'entité `Friendship` à ce DTO (charge les associations lazy).
|
||||
* Préférer {@link #FriendshipCreateOneResponseDTO(Friendship, UUID, UUID)} en fin de transaction.
|
||||
*/
|
||||
public FriendshipCreateOneResponseDTO(Friendship friendship) {
|
||||
this.id = friendship.getId();
|
||||
|
||||
@@ -36,11 +36,12 @@ public class FriendshipReadStatusResponseDTO {
|
||||
public FriendshipReadStatusResponseDTO(Friendship friendship) {
|
||||
this.friendshipId = friendship.getId();
|
||||
this.userId = friendship.getUser().getId();
|
||||
this.userNom = friendship.getUser().getNom();
|
||||
this.userPrenoms = friendship.getUser().getPrenoms();
|
||||
// v2.0 - Utiliser les nouveaux noms de champs
|
||||
this.userNom = friendship.getUser().getLastName();
|
||||
this.userPrenoms = friendship.getUser().getFirstName();
|
||||
this.friendId = friendship.getFriend().getId();
|
||||
this.friendNom = friendship.getFriend().getNom();
|
||||
this.friendPrenoms = friendship.getFriend().getPrenoms();
|
||||
this.friendNom = friendship.getFriend().getLastName();
|
||||
this.friendPrenoms = friendship.getFriend().getFirstName();
|
||||
this.status = friendship.getStatus();
|
||||
this.createdAt = friendship.getCreatedAt();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.lions.dev.dto.response.review;
|
||||
|
||||
import com.lions.dev.entity.establishment.Review;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour la réponse d'un avis.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReviewResponseDTO {
|
||||
|
||||
private UUID id;
|
||||
private UUID userId;
|
||||
private String userFirstName;
|
||||
private String userLastName;
|
||||
private String userProfileImageUrl;
|
||||
private UUID establishmentId;
|
||||
private String establishmentName;
|
||||
private Integer overallRating;
|
||||
private String comment;
|
||||
private Map<String, Integer> criteriaRatings;
|
||||
private Boolean isVerifiedVisit;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Constructeur à partir d'une entité Review.
|
||||
*
|
||||
* @param review L'entité Review
|
||||
*/
|
||||
public ReviewResponseDTO(Review review) {
|
||||
if (review != null) {
|
||||
this.id = review.getId();
|
||||
this.userId = review.getUser() != null ? review.getUser().getId() : null;
|
||||
this.userFirstName = review.getUser() != null ? review.getUser().getFirstName() : null;
|
||||
this.userLastName = review.getUser() != null ? review.getUser().getLastName() : null;
|
||||
this.userProfileImageUrl = review.getUser() != null ? review.getUser().getProfileImageUrl() : null;
|
||||
this.establishmentId = review.getEstablishment() != null ? review.getEstablishment().getId() : null;
|
||||
this.establishmentName = review.getEstablishment() != null ? review.getEstablishment().getName() : null;
|
||||
this.overallRating = review.getOverallRating();
|
||||
this.comment = review.getComment();
|
||||
this.criteriaRatings = review.getCriteriaRatings();
|
||||
this.isVerifiedVisit = review.getIsVerifiedVisit();
|
||||
this.createdAt = review.getCreatedAt();
|
||||
this.updatedAt = review.getUpdatedAt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom complet de l'auteur de l'avis.
|
||||
*/
|
||||
public String getUserFullName() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (userFirstName != null) {
|
||||
sb.append(userFirstName);
|
||||
}
|
||||
if (userLastName != null) {
|
||||
if (sb.length() > 0) sb.append(" ");
|
||||
sb.append(userLastName);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.lions.dev.dto.response.social;
|
||||
|
||||
import com.lions.dev.entity.social.PostComment;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO (Data Transfer Object) pour la réponse d'un commentaire de post social.
|
||||
*
|
||||
* Cette classe représente un commentaire avec les informations de l'auteur
|
||||
* pour l'affichage dans l'interface utilisateur.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PostCommentResponseDTO {
|
||||
|
||||
private UUID id;
|
||||
private String content;
|
||||
private UUID postId;
|
||||
private UUID userId;
|
||||
private String userFirstName;
|
||||
private String userLastName;
|
||||
private String userProfileImageUrl;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* Constructeur à partir d'une entité PostComment.
|
||||
*
|
||||
* @param comment L'entité PostComment
|
||||
*/
|
||||
public PostCommentResponseDTO(PostComment comment) {
|
||||
if (comment != null) {
|
||||
this.id = comment.getId();
|
||||
this.content = comment.getContent();
|
||||
this.postId = comment.getPost() != null ? comment.getPost().getId() : null;
|
||||
this.userId = comment.getUser() != null ? comment.getUser().getId() : null;
|
||||
this.userFirstName = comment.getUser() != null ? comment.getUser().getFirstName() : null;
|
||||
this.userLastName = comment.getUser() != null ? comment.getUser().getLastName() : null;
|
||||
this.userProfileImageUrl = comment.getUser() != null ? comment.getUser().getProfileImageUrl() : null;
|
||||
this.createdAt = comment.getCreatedAt();
|
||||
this.updatedAt = comment.getUpdatedAt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom complet de l'auteur du commentaire.
|
||||
*
|
||||
* @return Le nom complet (prénom + nom)
|
||||
*/
|
||||
public String getUserFullName() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (userFirstName != null) {
|
||||
sb.append(userFirstName);
|
||||
}
|
||||
if (userLastName != null) {
|
||||
if (sb.length() > 0) sb.append(" ");
|
||||
sb.append(userLastName);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user