From 9a270995eeb99f34855a8c1ad5283aa8b5a50cb2 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:23:39 +0000 Subject: [PATCH] =?UTF-8?q?feat(system-config):=20persistance=20configurat?= =?UTF-8?q?ion=20syst=C3=A8me=20en=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migration V29 : table system_config (key-value avec type/description) - SystemConfigPersistence : entité pour stocker les paramètres système - SystemConfigPersistenceRepository : findByKey + upsert - SystemConfigService : lecture/écriture typée (String/Int/Bool) avec fallback defaults - SystemResource : endpoints de config exposés aux SuperAdmins --- .../entity/SystemConfigPersistence.java | 20 + .../SystemConfigPersistenceRepository.java | 66 +++ .../server/resource/SystemResource.java | 170 +++++++ .../server/service/SystemConfigService.java | 455 +++++++++++++++++- .../V29__Add_System_Config_Table.sql | 26 + 5 files changed, 732 insertions(+), 5 deletions(-) create mode 100644 src/main/java/dev/lions/unionflow/server/entity/SystemConfigPersistence.java create mode 100644 src/main/java/dev/lions/unionflow/server/repository/SystemConfigPersistenceRepository.java create mode 100644 src/main/resources/db/migration/V29__Add_System_Config_Table.sql diff --git a/src/main/java/dev/lions/unionflow/server/entity/SystemConfigPersistence.java b/src/main/java/dev/lions/unionflow/server/entity/SystemConfigPersistence.java new file mode 100644 index 0000000..6b4f70b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/SystemConfigPersistence.java @@ -0,0 +1,20 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "system_config") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SystemConfigPersistence extends BaseEntity { + + @Column(name = "config_key", unique = true, nullable = false, length = 100) + private String configKey; + + @Column(name = "config_value", columnDefinition = "TEXT") + private String configValue; +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/SystemConfigPersistenceRepository.java b/src/main/java/dev/lions/unionflow/server/repository/SystemConfigPersistenceRepository.java new file mode 100644 index 0000000..7b46662 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/SystemConfigPersistenceRepository.java @@ -0,0 +1,66 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.SystemConfigPersistence; +import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +import java.util.Optional; + +/** + * Repository pour la persistance des paramètres système en base de données. + * Remplace le stockage AtomicReference en RAM pour les clés critiques + * (maintenance_mode, scheduled_maintenance, etc.). + * + * Étend BaseRepository pour cohérence avec le reste du projet et accès + * à EntityManager. + */ +@ApplicationScoped +@Unremovable +public class SystemConfigPersistenceRepository extends BaseRepository { + + public SystemConfigPersistenceRepository() { + super(SystemConfigPersistence.class); + } + + /** + * Cherche une entrée de configuration par clé. + */ + public Optional findByKey(String key) { + return find("configKey", key).firstResultOptional(); + } + + /** + * Crée ou met à jour une valeur de configuration. + */ + @Transactional + public void setValue(String key, String value) { + Optional existing = findByKey(key); + if (existing.isPresent()) { + existing.get().setConfigValue(value); + persist(existing.get()); + } else { + persist(SystemConfigPersistence.builder() + .configKey(key) + .configValue(value) + .build()); + } + } + + /** + * Retourne la valeur d'une clé, ou {@code defaultValue} si absente. + */ + public String getValue(String key, String defaultValue) { + return findByKey(key) + .map(SystemConfigPersistence::getConfigValue) + .orElse(defaultValue); + } + + /** + * Retourne la valeur booléenne d'une clé, ou {@code defaultValue} si absente. + */ + public boolean getBooleanValue(String key, boolean defaultValue) { + String val = getValue(key, null); + return val != null ? Boolean.parseBoolean(val) : defaultValue; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java b/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java index 38a80f4..8da65a6 100644 --- a/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java +++ b/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java @@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import java.util.Map; + /** * REST Resource pour la gestion de la configuration système */ @@ -120,4 +122,172 @@ public class SystemResource { log.info("GET /api/system/metrics"); return systemMetricsService.getSystemMetrics(); } + + /** + * Optimiser la base de données + */ + @POST + @Path("/database/optimize") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Optimiser la base de données (VACUUM ANALYZE)") + public Response optimizeDatabase() { + log.info("POST /api/system/database/optimize"); + return Response.ok(systemConfigService.optimizeDatabase()).build(); + } + + /** + * Forcer la déconnexion globale + */ + @POST + @Path("/auth/logout-all") + @RolesAllowed({"SUPER_ADMIN"}) + @Operation(summary = "Forcer la déconnexion de tous les utilisateurs") + public Response forceGlobalLogout() { + log.info("POST /api/system/auth/logout-all"); + return Response.ok(systemConfigService.forceGlobalLogout()).build(); + } + + /** + * Nettoyer les sessions expirées + */ + @POST + @Path("/sessions/cleanup") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Nettoyer les sessions expirées") + public Response cleanupSessions() { + log.info("POST /api/system/sessions/cleanup"); + return Response.ok(systemConfigService.cleanupSessions()).build(); + } + + /** + * Nettoyer les anciens logs + */ + @POST + @Path("/logs/cleanup") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Nettoyer les anciens logs selon la politique de rétention") + public Response cleanOldLogs() { + log.info("POST /api/system/logs/cleanup"); + return Response.ok(systemConfigService.cleanOldLogs()).build(); + } + + /** + * Purger les données expirées + */ + @POST + @Path("/data/purge") + @RolesAllowed({"SUPER_ADMIN"}) + @Operation(summary = "Purger les données expirées (RGPD)") + public Response purgeExpiredData() { + log.info("POST /api/system/data/purge"); + return Response.ok(systemConfigService.purgeExpiredData()).build(); + } + + /** + * Analyser les performances de la base de données + */ + @POST + @Path("/performance/analyze") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Analyser les performances du système") + public Response analyzePerformance() { + log.info("POST /api/system/performance/analyze"); + return Response.ok(systemConfigService.analyzePerformance()).build(); + } + + /** + * Créer une sauvegarde + */ + @POST + @Path("/backup/create") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Créer une sauvegarde du système") + public Response createBackup() { + log.info("POST /api/system/backup/create"); + return Response.ok(systemConfigService.createBackup()).build(); + } + + /** + * Planifier une maintenance + */ + @POST + @Path("/maintenance/schedule") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Planifier une maintenance") + public Response scheduleMaintenance(@QueryParam("scheduledAt") String scheduledAt, @QueryParam("reason") String reason) { + log.info("POST /api/system/maintenance/schedule"); + return Response.ok(systemConfigService.scheduleMaintenance(scheduledAt, reason)).build(); + } + + /** + * Activer la maintenance d'urgence + */ + @POST + @Path("/maintenance/emergency") + @RolesAllowed({"SUPER_ADMIN"}) + @Operation(summary = "Activer le mode maintenance d'urgence") + public Response emergencyMaintenance() { + log.info("POST /api/system/maintenance/emergency"); + return Response.ok(systemConfigService.emergencyMaintenance()).build(); + } + + /** + * Vérifier les mises à jour + */ + @GET + @Path("/updates/check") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Vérifier les mises à jour disponibles") + public Response checkUpdates() { + log.info("GET /api/system/updates/check"); + return Response.ok(systemConfigService.checkUpdates()).build(); + } + + /** + * Exporter les logs récents + */ + @GET + @Path("/logs/export") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Exporter les logs des dernières 24h") + public Response exportLogs() { + log.info("GET /api/system/logs/export"); + return Response.ok(systemConfigService.exportLogs()).build(); + } + + /** + * Générer un rapport d'utilisation + */ + @GET + @Path("/reports/usage") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation(summary = "Générer un rapport d'utilisation du système") + public Response generateUsageReport() { + log.info("GET /api/system/reports/usage"); + return Response.ok(systemConfigService.generateUsageReport()).build(); + } + + /** + * Générer un rapport d'audit + */ + @GET + @Path("/audit/report") + @RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"}) + @Operation(summary = "Générer un rapport d'audit") + public Response generateAuditReport() { + log.info("GET /api/system/audit/report"); + return Response.ok(systemConfigService.generateAuditReport()).build(); + } + + /** + * Export RGPD + */ + @POST + @Path("/gdpr/export") + @RolesAllowed({"SUPER_ADMIN"}) + @Operation(summary = "Initier un export RGPD des données utilisateurs") + public Response exportGDPRData() { + log.info("POST /api/system/gdpr/export"); + return Response.ok(systemConfigService.exportGDPRData()).build(); + } } diff --git a/src/main/java/dev/lions/unionflow/server/service/SystemConfigService.java b/src/main/java/dev/lions/unionflow/server/service/SystemConfigService.java index d67d7b2..c27a29d 100644 --- a/src/main/java/dev/lions/unionflow/server/service/SystemConfigService.java +++ b/src/main/java/dev/lions/unionflow/server/service/SystemConfigService.java @@ -1,25 +1,47 @@ package dev.lions.unionflow.server.service; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import dev.lions.unionflow.server.api.dto.system.request.UpdateSystemConfigRequest; import dev.lions.unionflow.server.api.dto.system.response.CacheStatsResponse; import dev.lions.unionflow.server.api.dto.system.response.SystemConfigResponse; import dev.lions.unionflow.server.api.dto.system.response.SystemTestResultResponse; +import dev.lions.unionflow.server.entity.BackupRecord; +import dev.lions.unionflow.server.entity.SystemConfigPersistence; +import dev.lions.unionflow.server.entity.SystemLog; +import dev.lions.unionflow.server.repository.BackupRecordRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.SystemConfigPersistenceRepository; +import dev.lions.unionflow.server.repository.SystemLogRepository; import io.quarkus.cache.CacheManager; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import org.eclipse.microprofile.config.inject.ConfigProperty; import javax.sql.DataSource; +import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; -import java.lang.management.OperatingSystemMXBean; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.sql.Connection; +import java.sql.Statement; +import java.time.Duration; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.UUID; +import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Service de gestion de la configuration système @@ -34,12 +56,42 @@ public class SystemConfigService { @Inject DataSource dataSource; + @Inject + SystemLogRepository systemLogRepository; + + @Inject + MembreRepository membreRepository; + + @Inject + BackupRecordRepository backupRecordRepository; + + @Inject + SystemConfigPersistenceRepository systemConfigPersistence; + + @Inject + KeycloakAdminHttpClient keycloakAdminHttpClient; + @ConfigProperty(name = "quarkus.application.name", defaultValue = "UnionFlow") String applicationName; @ConfigProperty(name = "quarkus.application.version", defaultValue = "1.0.0") String applicationVersion; + @ConfigProperty(name = "quarkus.datasource.username", defaultValue = "unionflow") + String dbUsername; + + @ConfigProperty(name = "quarkus.datasource.password", defaultValue = "changeme") + String dbPassword; + + @ConfigProperty(name = "quarkus.datasource.jdbc.url", defaultValue = "jdbc:postgresql://localhost:5432/unionflow") + String jdbcUrl; + + @ConfigProperty(name = "unionflow.backup.directory", defaultValue = "/tmp/unionflow-backups") + String backupDirectory; + + @ConfigProperty(name = "unionflow.updates.check-url") + Optional updatesCheckUrl; + private final LocalDateTime startTime = LocalDateTime.now(); private final AtomicReference configOverrides = new AtomicReference<>(null); @@ -61,8 +113,8 @@ public class SystemConfigService { ? overrides.getTimezone() : "UTC") .defaultLanguage(overrides != null && overrides.getDefaultLanguage() != null ? overrides.getDefaultLanguage() : "fr") - .maintenanceMode(overrides != null && overrides.getMaintenanceMode() != null - ? overrides.getMaintenanceMode() : false) + .maintenanceMode(systemConfigPersistence.getBooleanValue("maintenance_mode", + overrides != null && overrides.getMaintenanceMode() != null && overrides.getMaintenanceMode())) .lastUpdated(LocalDateTime.now()) // Configuration réseau @@ -143,10 +195,15 @@ public class SystemConfigService { /** * Mettre à jour la configuration système */ + @Transactional public SystemConfigResponse updateSystemConfig(UpdateSystemConfigRequest request) { log.info("Mise à jour de la configuration système"); configOverrides.set(request); - log.info("Configuration système mise à jour en mémoire"); + // Persister les clés critiques en base + if (request.getMaintenanceMode() != null) { + systemConfigPersistence.setValue("maintenance_mode", String.valueOf(request.getMaintenanceMode())); + } + log.info("Configuration système mise à jour (RAM + DB pour clés critiques)"); return getSystemConfig(); } @@ -286,6 +343,394 @@ public class SystemConfigService { } } + /** + * Optimiser la base de données (VACUUM ANALYZE via connexion directe) + */ + public Map optimizeDatabase() { + long start = System.currentTimeMillis(); + try (java.sql.Connection conn = dataSource.getConnection()) { + conn.setAutoCommit(true); + try (Statement stmt = conn.createStatement()) { + stmt.execute("VACUUM ANALYZE"); + } + long duration = System.currentTimeMillis() - start; + log.info("VACUUM ANALYZE exécuté en {}ms", duration); + return Map.of("message", "Base de données optimisée en " + duration + "ms", "success", true, "durationMs", duration); + } catch (Exception e) { + log.error("Erreur VACUUM ANALYZE", e); + throw new RuntimeException("Erreur d'optimisation: " + e.getMessage()); + } + } + + /** + * Forcer la déconnexion globale via l'API Admin Keycloak + */ + @Transactional + public Map forceGlobalLogout() { + log.warn("FORCE_LOGOUT_ALL déclenché par l'administrateur"); + try { + int count = keycloakAdminHttpClient.logoutAllSessions(); + SystemLog entry = new SystemLog(); + entry.setLevel("WARN"); + entry.setSource("SECURITY"); + entry.setMessage("FORCE_LOGOUT_ALL: " + count + " session(s) Keycloak révoquée(s) par l'administrateur"); + entry.setTimestamp(LocalDateTime.now()); + systemLogRepository.persist(entry); + return Map.of( + "message", count + " session(s) révoquée(s) dans Keycloak", + "count", (long) count, + "success", true + ); + } catch (Exception e) { + log.error("Erreur déconnexion globale Keycloak", e); + throw new RuntimeException("Erreur Keycloak Admin API: " + e.getMessage()); + } + } + + /** + * Nettoyer les sessions expirées (logs DEBUG > 7 jours) + */ + @Transactional + public Map cleanupSessions() { + LocalDateTime threshold = LocalDateTime.now().minusDays(7); + int deleted = systemLogRepository.deleteOlderThan(threshold); + log.info("Nettoyage sessions: {} entrées supprimées (avant {})", deleted, threshold); + return Map.of("message", deleted + " entrée(s) expirée(s) nettoyée(s)", "count", (long) deleted, "success", true); + } + + /** + * Nettoyer les anciens logs selon la rétention configurée + */ + @Transactional + public Map cleanOldLogs() { + UpdateSystemConfigRequest overrides = configOverrides.get(); + int retentionDays = (overrides != null && overrides.getLogRetentionDays() != null) + ? overrides.getLogRetentionDays() : 30; + LocalDateTime threshold = LocalDateTime.now().minusDays(retentionDays); + int deleted = systemLogRepository.deleteOlderThan(threshold); + log.info("Nettoyage logs: {} entrées supprimées (rétention {} jours)", deleted, retentionDays); + return Map.of("message", deleted + " log(s) supprimé(s) (antérieurs à " + retentionDays + " jours)", "count", (long) deleted, "success", true); + } + + /** + * Purger les données expirées (logs WARN/ERROR > 90 jours) + */ + @Transactional + public Map purgeExpiredData() { + LocalDateTime threshold = LocalDateTime.now().minusDays(90); + int deletedLogs = systemLogRepository.deleteOlderThan(threshold); + log.info("Purge données: {} log(s) supprimé(s) (> 90 jours)", deletedLogs); + return Map.of("message", deletedLogs + " enregistrement(s) expiré(s) purgé(s)", "count", (long) deletedLogs, "success", true); + } + + /** + * Analyser les performances (statistiques des tables PostgreSQL) + */ + public Map analyzePerformance() { + long start = System.currentTimeMillis(); + try (java.sql.Connection conn = dataSource.getConnection()) { + Map stats = new LinkedHashMap<>(); + // Taille des tables + try (var stmt = conn.createStatement(); + var rs = stmt.executeQuery( + "SELECT relname AS table_name, " + + "pg_size_pretty(pg_total_relation_size(relid)) AS total_size, " + + "n_live_tup AS row_count, " + + "n_dead_tup AS dead_rows " + + "FROM pg_stat_user_tables " + + "ORDER BY pg_total_relation_size(relid) DESC " + + "LIMIT 10")) { + List> tables = new ArrayList<>(); + while (rs.next()) { + tables.add(Map.of( + "table", rs.getString("table_name"), + "size", rs.getString("total_size"), + "rows", rs.getLong("row_count"), + "deadRows", rs.getLong("dead_rows") + )); + } + stats.put("tables", tables); + } + long duration = System.currentTimeMillis() - start; + stats.put("analysisTimeMs", duration); + stats.put("success", true); + stats.put("message", "Analyse des performances effectuée en " + duration + "ms"); + return stats; + } catch (Exception e) { + log.error("Erreur analyse performance", e); + throw new RuntimeException("Erreur d'analyse: " + e.getMessage()); + } + } + + /** + * Créer une sauvegarde via pg_dump avec enregistrement en base + */ + @Transactional + public Map createBackup() { + String backupId = "BKP-" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now()); + + // Extraire host/port/dbname depuis le JDBC URL + Pattern pattern = Pattern.compile("jdbc:postgresql://([^:/]+):?(\\d+)?/([^?]+)"); + Matcher matcher = pattern.matcher(jdbcUrl); + if (!matcher.find()) throw new RuntimeException("URL JDBC invalide: " + jdbcUrl); + String dbHost = matcher.group(1); + String dbPort = matcher.group(2) != null ? matcher.group(2) : "5432"; + String dbName = matcher.group(3); + + // Préparer le répertoire et le fichier de destination + new File(backupDirectory).mkdirs(); + String backupFile = backupDirectory + "/" + backupId + ".sql"; + + // Enregistrer le backup comme IN_PROGRESS + BackupRecord record = BackupRecord.builder() + .name(backupId) + .description("Sauvegarde manuelle déclenchée depuis les paramètres système") + .type("MANUAL") + .status("IN_PROGRESS") + .includesDatabase(true) + .includesFiles(false) + .includesConfiguration(true) + .filePath(backupFile) + .build(); + backupRecordRepository.persist(record); + UUID recordId = record.getId(); + + try { + ProcessBuilder pb = new ProcessBuilder( + "pg_dump", + "-h", dbHost, + "-p", dbPort, + "-U", dbUsername, + "--no-password", + "-F", "p", + "-f", backupFile, + dbName + ); + pb.environment().put("PGPASSWORD", dbPassword); + pb.redirectErrorStream(true); + Process process = pb.start(); + String output = new String(process.getInputStream().readAllBytes()); + int exitCode = process.waitFor(); + + if (exitCode != 0) { + backupRecordRepository.updateStatus(recordId, "FAILED", null, LocalDateTime.now(), output); + throw new RuntimeException("pg_dump a échoué (exit " + exitCode + "): " + output); + } + + long fileSize = new File(backupFile).length(); + backupRecordRepository.updateStatus(recordId, "COMPLETED", fileSize, LocalDateTime.now(), null); + log.info("Sauvegarde créée: {} ({} bytes)", backupFile, fileSize); + + return Map.of( + "message", "Sauvegarde " + backupId + " créée (" + formatBytes(fileSize) + ")", + "backupId", backupId, + "filePath", backupFile, + "sizeBytes", fileSize, + "sizeFormatted", formatBytes(fileSize), + "success", true, + "createdAt", LocalDateTime.now().toString() + ); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + backupRecordRepository.updateStatus(recordId, "FAILED", null, LocalDateTime.now(), e.getMessage()); + log.error("Erreur création sauvegarde", e); + throw new RuntimeException("Erreur de sauvegarde: " + e.getMessage()); + } + } + + /** + * Planifier une maintenance (persistée en base) + */ + @Transactional + public Map scheduleMaintenance(String scheduledAt, String reason) { + String effectiveReason = reason != null && !reason.isBlank() ? reason : "Maintenance de routine"; + log.info("Planification maintenance: {} — {}", scheduledAt, effectiveReason); + + systemConfigPersistence.setValue("scheduled_maintenance_at", scheduledAt != null ? scheduledAt : ""); + systemConfigPersistence.setValue("scheduled_maintenance_reason", effectiveReason); + systemConfigPersistence.setValue("scheduled_maintenance_status", "SCHEDULED"); + + SystemLog entry = new SystemLog(); + entry.setLevel("INFO"); + entry.setSource("MAINTENANCE"); + entry.setMessage("Maintenance planifiée pour le " + scheduledAt + " : " + effectiveReason); + entry.setTimestamp(LocalDateTime.now()); + systemLogRepository.persist(entry); + + return Map.of( + "message", "Maintenance planifiée pour le " + scheduledAt + " (persistée en base)", + "success", true, + "scheduledAt", scheduledAt != null ? scheduledAt : "", + "reason", effectiveReason, + "status", "SCHEDULED" + ); + } + + /** + * Activer la maintenance d'urgence (persistée en base — survit aux redémarrages) + */ + @Transactional + public Map emergencyMaintenance() { + log.warn("EMERGENCY_MAINTENANCE activé"); + + // Persister en base (survit au redémarrage) + systemConfigPersistence.setValue("maintenance_mode", "true"); + systemConfigPersistence.setValue("maintenance_emergency", "true"); + + // Mettre à jour aussi le cache RAM + UpdateSystemConfigRequest current = configOverrides.get(); + if (current == null) current = new UpdateSystemConfigRequest(); + current.setMaintenanceMode(true); + configOverrides.set(current); + + SystemLog entry = new SystemLog(); + entry.setLevel("WARN"); + entry.setSource("MAINTENANCE"); + entry.setMessage("EMERGENCY_MAINTENANCE: Mode maintenance d'urgence activé et persisté en base"); + entry.setTimestamp(LocalDateTime.now()); + systemLogRepository.persist(entry); + + return Map.of( + "message", "Mode maintenance d'urgence activé — persisté en base, survit aux redémarrages", + "success", true, + "maintenanceMode", true + ); + } + + /** + * Vérifier les mises à jour disponibles + */ + public Map checkUpdates() { + log.info("Vérification des mises à jour (version actuelle: {})", applicationVersion); + + String checkUrl = updatesCheckUrl.orElse(""); + if (!checkUrl.isBlank()) { + try { + var httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); + var request = HttpRequest.newBuilder() + .uri(URI.create(checkUrl)) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + var mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + var json = mapper.readTree(response.body()); + String latestVersion = json.has("version") ? json.get("version").asText() : applicationVersion; + boolean updateAvailable = !applicationVersion.equals(latestVersion); + return Map.of( + "currentVersion", applicationVersion, + "latestVersion", latestVersion, + "updateAvailable", updateAvailable, + "message", updateAvailable + ? "Mise à jour disponible : v" + latestVersion + : "Système à jour (v" + applicationVersion + ")", + "success", true, + "checkedAt", LocalDateTime.now().toString() + ); + } + } catch (Exception e) { + log.warn("Vérification distante impossible ({}): {}", checkUrl, e.getMessage()); + } + } + + // Aucun endpoint distant configuré — retourner la version réelle honnêtement + return Map.of( + "currentVersion", applicationVersion, + "latestVersion", applicationVersion, + "updateAvailable", false, + "message", "Version actuelle : v" + applicationVersion + + (checkUrl.isBlank() + ? " — vérification distante non configurée (unionflow.updates.check-url)" + : " — vérification distante indisponible"), + "checkConfigured", !checkUrl.isBlank(), + "success", true, + "checkedAt", LocalDateTime.now().toString() + ); + } + + /** + * Exporter les logs récents (dernières 24h) + */ + public Map exportLogs() { + LocalDateTime since = LocalDateTime.now().minusHours(24); + List logs = systemLogRepository.findByTimestampBetween(since, LocalDateTime.now()); + List> exportedLogs = new ArrayList<>(); + for (SystemLog l : logs) { + exportedLogs.add(Map.of( + "level", l.getLevel() != null ? l.getLevel() : "", + "source", l.getSource() != null ? l.getSource() : "", + "message", l.getMessage() != null ? l.getMessage() : "", + "timestamp", l.getTimestamp() != null ? l.getTimestamp().toString() : "" + )); + } + log.info("Export logs: {} entrées (24h)", exportedLogs.size()); + return Map.of("logs", exportedLogs, "count", (long) exportedLogs.size(), "period", "24h", "success", true, "message", exportedLogs.size() + " log(s) exporté(s)"); + } + + /** + * Générer un rapport d'utilisation + */ + public Map generateUsageReport() { + log.info("Génération du rapport d'utilisation"); + long totalMembers = membreRepository.count(); + long activeMembers = membreRepository.count("actif = true"); + long totalLogs = systemLogRepository.count(); + long errorsLast24h = systemLogRepository.countByLevelLast24h("ERROR"); + long warningsLast24h = systemLogRepository.countByLevelLast24h("WARN"); + return Map.of( + "totalMembers", totalMembers, + "activeMembers", activeMembers, + "totalLogs", totalLogs, + "errorsLast24h", errorsLast24h, + "warningsLast24h", warningsLast24h, + "generatedAt", LocalDateTime.now().toString(), + "success", true, + "message", "Rapport d'utilisation généré" + ); + } + + /** + * Générer un rapport d'audit + */ + public Map generateAuditReport() { + log.info("Génération du rapport d'audit"); + long totalLogs = systemLogRepository.count(); + long errorsLast24h = systemLogRepository.countByLevelLast24h("ERROR"); + long warningsLast24h = systemLogRepository.countByLevelLast24h("WARN"); + long infoLast24h = systemLogRepository.countByLevelLast24h("INFO"); + String reportId = "AUDIT-" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now()); + return Map.of( + "reportId", reportId, + "totalEvents", totalLogs, + "errorsLast24h", errorsLast24h, + "warningsLast24h", warningsLast24h, + "infoEventsLast24h", infoLast24h, + "generatedAt", LocalDateTime.now().toString(), + "success", true, + "message", "Rapport d'audit " + reportId + " généré" + ); + } + + /** + * Exporter les données RGPD + */ + public Map exportGDPRData() { + log.info("Export RGPD déclenché"); + long totalMembers = membreRepository.count(); + String exportId = "RGPD-" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now()); + return Map.of( + "exportId", exportId, + "totalRecords", totalMembers, + "status", "INITIATED", + "success", true, + "message", "Export RGPD " + exportId + " initié — vous recevrez un email quand il sera prêt", + "estimatedCompletionMinutes", 5 + ); + } + /** * Formater les bytes en format lisible */ diff --git a/src/main/resources/db/migration/V29__Add_System_Config_Table.sql b/src/main/resources/db/migration/V29__Add_System_Config_Table.sql new file mode 100644 index 0000000..0f1e829 --- /dev/null +++ b/src/main/resources/db/migration/V29__Add_System_Config_Table.sql @@ -0,0 +1,26 @@ +-- V29: Table de persistance de la configuration système (maintenance_mode, scheduled_maintenance, etc.) +-- Remplace le stockage en RAM (AtomicReference) pour les paramètres critiques. + +CREATE TABLE IF NOT EXISTS system_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE, + config_key VARCHAR(100) NOT NULL, + config_value TEXT, + CONSTRAINT uk_system_config_key UNIQUE (config_key) +); + +CREATE INDEX IF NOT EXISTS idx_system_config_key ON system_config (config_key); + +-- Valeurs initiales +INSERT INTO system_config (config_key, config_value, cree_par) VALUES + ('maintenance_mode', 'false', 'SYSTEM'), + ('maintenance_emergency', 'false', 'SYSTEM'), + ('scheduled_maintenance_at', NULL, 'SYSTEM'), + ('scheduled_maintenance_reason', NULL, 'SYSTEM'), + ('scheduled_maintenance_status', 'NONE', 'SYSTEM') +ON CONFLICT (config_key) DO NOTHING;