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:
dahoud
2026-04-15 20:23:39 +00:00
parent 217021933e
commit 9a270995ee
5 changed files with 732 additions and 5 deletions

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<String> updatesCheckUrl;
private final LocalDateTime startTime = LocalDateTime.now();
private final AtomicReference<UpdateSystemConfigRequest> 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<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
*/

View File

@@ -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;