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