feat(system-config): persistance configuration système en DB
- 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
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<SystemConfigPersistence> {
|
||||||
|
|
||||||
|
public SystemConfigPersistenceRepository() {
|
||||||
|
super(SystemConfigPersistence.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cherche une entrée de configuration par clé.
|
||||||
|
*/
|
||||||
|
public Optional<SystemConfigPersistence> 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<SystemConfigPersistence> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST Resource pour la gestion de la configuration système
|
* REST Resource pour la gestion de la configuration système
|
||||||
*/
|
*/
|
||||||
@@ -120,4 +122,172 @@ public class SystemResource {
|
|||||||
log.info("GET /api/system/metrics");
|
log.info("GET /api/system/metrics");
|
||||||
return systemMetricsService.getSystemMetrics();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,47 @@
|
|||||||
package dev.lions.unionflow.server.service;
|
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.request.UpdateSystemConfigRequest;
|
||||||
import dev.lions.unionflow.server.api.dto.system.response.CacheStatsResponse;
|
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.SystemConfigResponse;
|
||||||
import dev.lions.unionflow.server.api.dto.system.response.SystemTestResultResponse;
|
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 io.quarkus.cache.CacheManager;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
import java.io.File;
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.lang.management.MemoryMXBean;
|
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.Connection;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service de gestion de la configuration système
|
* Service de gestion de la configuration système
|
||||||
@@ -34,12 +56,42 @@ public class SystemConfigService {
|
|||||||
@Inject
|
@Inject
|
||||||
DataSource dataSource;
|
DataSource dataSource;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SystemLogRepository systemLogRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MembreRepository membreRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BackupRecordRepository backupRecordRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SystemConfigPersistenceRepository systemConfigPersistence;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
KeycloakAdminHttpClient keycloakAdminHttpClient;
|
||||||
|
|
||||||
@ConfigProperty(name = "quarkus.application.name", defaultValue = "UnionFlow")
|
@ConfigProperty(name = "quarkus.application.name", defaultValue = "UnionFlow")
|
||||||
String applicationName;
|
String applicationName;
|
||||||
|
|
||||||
@ConfigProperty(name = "quarkus.application.version", defaultValue = "1.0.0")
|
@ConfigProperty(name = "quarkus.application.version", defaultValue = "1.0.0")
|
||||||
String applicationVersion;
|
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<String> updatesCheckUrl;
|
||||||
|
|
||||||
private final LocalDateTime startTime = LocalDateTime.now();
|
private final LocalDateTime startTime = LocalDateTime.now();
|
||||||
private final AtomicReference<UpdateSystemConfigRequest> configOverrides = new AtomicReference<>(null);
|
private final AtomicReference<UpdateSystemConfigRequest> configOverrides = new AtomicReference<>(null);
|
||||||
|
|
||||||
@@ -61,8 +113,8 @@ public class SystemConfigService {
|
|||||||
? overrides.getTimezone() : "UTC")
|
? overrides.getTimezone() : "UTC")
|
||||||
.defaultLanguage(overrides != null && overrides.getDefaultLanguage() != null
|
.defaultLanguage(overrides != null && overrides.getDefaultLanguage() != null
|
||||||
? overrides.getDefaultLanguage() : "fr")
|
? overrides.getDefaultLanguage() : "fr")
|
||||||
.maintenanceMode(overrides != null && overrides.getMaintenanceMode() != null
|
.maintenanceMode(systemConfigPersistence.getBooleanValue("maintenance_mode",
|
||||||
? overrides.getMaintenanceMode() : false)
|
overrides != null && overrides.getMaintenanceMode() != null && overrides.getMaintenanceMode()))
|
||||||
.lastUpdated(LocalDateTime.now())
|
.lastUpdated(LocalDateTime.now())
|
||||||
|
|
||||||
// Configuration réseau
|
// Configuration réseau
|
||||||
@@ -143,10 +195,15 @@ public class SystemConfigService {
|
|||||||
/**
|
/**
|
||||||
* Mettre à jour la configuration système
|
* Mettre à jour la configuration système
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public SystemConfigResponse updateSystemConfig(UpdateSystemConfigRequest request) {
|
public SystemConfigResponse updateSystemConfig(UpdateSystemConfigRequest request) {
|
||||||
log.info("Mise à jour de la configuration système");
|
log.info("Mise à jour de la configuration système");
|
||||||
configOverrides.set(request);
|
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();
|
return getSystemConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +343,394 @@ public class SystemConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimiser la base de données (VACUUM ANALYZE via connexion directe)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> analyzePerformance() {
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
try (java.sql.Connection conn = dataSource.getConnection()) {
|
||||||
|
Map<String, Object> 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<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> exportLogs() {
|
||||||
|
LocalDateTime since = LocalDateTime.now().minusHours(24);
|
||||||
|
List<SystemLog> logs = systemLogRepository.findByTimestampBetween(since, LocalDateTime.now());
|
||||||
|
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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
|
* Formater les bytes en format lisible
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user