diff --git a/app/public/css/dark-mode.css b/app/public/css/dark-mode.css
new file mode 100644
index 0000000..7f5a78e
--- /dev/null
+++ b/app/public/css/dark-mode.css
@@ -0,0 +1,57 @@
+/* Styles pour le mode sombre */
+body.dark-mode {
+ --bg-color: #2c2c2c;
+ --text-color: #e0e0e0;
+ --container-bg: #3a3a3a;
+ --container-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ --header-bg: #1a4d7c;
+ --header-text: #f0f0f0;
+ --nav-bg: #444;
+ --nav-link-color: #9cb3cc;
+ --nav-link-hover-bg: #555;
+ --table-border-color: #555;
+ --table-header-bg: #1a4d7c;
+ --table-header-text: #fff;
+ --table-row-even-bg: #333;
+ --button-primary-bg: #0056b3;
+ --button-primary-hover-bg: #004085;
+ --button-secondary-bg: #5a6268;
+ --button-secondary-hover-bg: #43484f;
+ --button-danger-bg: #a71d2a;
+ --button-danger-hover-bg: #7a151f;
+ --status-revoked-color: #ff6666;
+ --status-active-color: #66ff66;
+ --message-success-color: #a3e6a3;
+ --message-error-color: #ff9999;
+ --input-border: #666;
+ --input-focus-border: #007bff;
+}
+
+body.dark-mode .app-footer {
+ background-color: #1a1a1a;
+ color: #ccc;
+}
+
+body.dark-mode tr.revoked-cert {
+ background-color: #5c2c2c; /* Fond plus foncé pour le mode sombre */
+ color: #aaa;
+}
+body.dark-mode tr.revoked-cert:hover {
+ background-color: #703c3c;
+}
+
+/* Spécifiques pour les inputs en mode sombre */
+body.dark-mode form input[type="text"],
+body.dark-mode form input[type="password"],
+body.dark-mode form select {
+ background-color: #4a4a4a;
+ color: var(--text-color);
+ border: 1px solid var(--input-border);
+}
+
+body.dark-mode form input[type="text"]:focus,
+body.dark-mode form input[type="password"]:focus,
+body.dark-mode form select:focus {
+ border-color: var(--input-focus-border);
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.4);
+}
diff --git a/app/public/css/style.css b/app/public/css/style.css
new file mode 100644
index 0000000..976be67
--- /dev/null
+++ b/app/public/css/style.css
@@ -0,0 +1,366 @@
+/* Variables CSS pour faciliter le basculement entre les thèmes */
+:root {
+ --bg-color: #f4f4f4;
+ --text-color: #333;
+ --container-bg: #fff;
+ --container-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ --header-bg: #0056b3;
+ --header-text: #fff;
+ --nav-bg: #e2e2e2;
+ --nav-link-color: #0056b3;
+ --nav-link-hover-bg: #d1d1d1;
+ --table-border-color: #ddd;
+ --table-header-bg: #0056b3;
+ --table-header-text: #fff;
+ --table-row-even-bg: #f2f2f2;
+ --button-primary-bg: #007bff;
+ --button-primary-hover-bg: #0056b3;
+ --button-secondary-bg: #6c757d;
+ --button-secondary-hover-bg: #5a6268;
+ --button-danger-bg: #dc3545;
+ --button-danger-hover-bg: #bd2130;
+ --status-revoked-color: red;
+ --status-active-color: green;
+ --message-success-color: green;
+ --message-error-color: red;
+ --input-border: #ccc;
+ --input-focus-border: #007bff;
+}
+
+/* Styles généraux */
+body {
+ font-family: 'Inter', sans-serif; /* Utilisation de Inter comme demandé */
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ line-height: 1.6;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 960px;
+ margin: 20px auto;
+ background-color: var(--container-bg);
+ padding: 20px 30px;
+ border-radius: 12px; /* Coins arrondis */
+ box-shadow: var(--container-shadow);
+ flex-grow: 1; /* Permet au container de prendre de l'espace */
+}
+
+h1, h2 {
+ color: var(--header-bg);
+ margin-top: 0;
+ margin-bottom: 20px;
+}
+
+/* En-tête de l'application */
+.app-header {
+ background-color: var(--header-bg);
+ color: var(--header-text);
+ padding: 15px 0;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+.app-title h1 {
+ color: var(--header-text);
+ margin: 0;
+ font-size: 1.8em;
+}
+
+.header-controls {
+ display: flex;
+ gap: 15px;
+}
+
+.language-switcher, .dark-mode-switcher {
+ display: flex;
+ align-items: center;
+}
+
+.lang-button, .dark-mode-button {
+ padding: 8px 12px;
+ text-decoration: none;
+ color: var(--header-text);
+ border: 1px solid rgba(255, 255, 255, 0.4);
+ border-radius: 8px; /* Coins arrondis */
+ transition: background-color 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.lang-button:hover, .dark-mode-button:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.lang-button.active {
+ background-color: rgba(255, 255, 255, 0.3);
+ border-color: var(--header-text);
+}
+
+/* Styles de navigation */
+nav ul {
+ list-style: none;
+ padding: 0;
+ margin: 20px 0;
+ background-color: var(--nav-bg);
+ border-radius: 10px; /* Coins arrondis */
+ display: flex;
+ justify-content: center;
+ overflow: hidden; /* Pour que les coins arrondis fonctionnent bien avec le hover */
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
+}
+
+nav ul li {
+ margin: 0;
+}
+
+nav ul li a {
+ display: block;
+ padding: 12px 20px;
+ text-decoration: none;
+ color: var(--nav-link-color);
+ font-weight: bold;
+ transition: background-color 0.3s ease, color 0.3s ease;
+ border-radius: 8px; /* Appliquer aux liens pour le hover */
+}
+
+nav ul li a:hover {
+ background-color: var(--nav-link-hover-bg);
+ color: var(--button-primary-hover-bg); /* Ou une autre couleur contrastante */
+}
+
+/* Styles des messages */
+.success-message {
+ background-color: #d4edda;
+ color: var(--message-success-color);
+ border: 1px solid #c3e6cb;
+ padding: 10px;
+ border-radius: 8px;
+ margin-bottom: 15px;
+}
+
+.error-message {
+ background-color: #f8d7da;
+ color: var(--message-error-color);
+ border: 1px solid #f5c6cb;
+ padding: 10px;
+ border-radius: 8px;
+ margin-bottom: 15px;
+}
+
+/* Styles de tableau */
+.table-responsive {
+ overflow-x: auto; /* Pour les petits écrans */
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 20px;
+ border-radius: 12px; /* Coins arrondis pour le tableau */
+ overflow: hidden; /* Important pour que les coins arrondis soient visibles */
+}
+
+table, th, td {
+ border: 1px solid var(--table-border-color);
+}
+
+th, td {
+ padding: 12px 15px;
+ text-align: left;
+}
+
+th {
+ background-color: var(--table-header-bg);
+ color: var(--table-header-text);
+ font-weight: bold;
+}
+
+tr:nth-child(even) {
+ background-color: var(--table-row-even-bg);
+}
+
+tr:hover {
+ background-color: rgba(0, 0, 0, 0.05); /* Léger survol */
+}
+
+/* Styles des boutons */
+.button {
+ display: inline-block;
+ padding: 10px 20px;
+ font-size: 1em;
+ text-decoration: none;
+ color: white;
+ border-radius: 8px; /* Coins arrondis */
+ transition: background-color 0.3s ease, transform 0.2s ease;
+ border: none;
+ cursor: pointer;
+ text-align: center;
+}
+
+.button:active {
+ transform: translateY(1px);
+}
+
+.primary-button {
+ background-color: var(--button-primary-bg);
+}
+
+.primary-button:hover {
+ background-color: var(--button-primary-hover-bg);
+}
+
+.secondary-button {
+ background-color: var(--button-secondary-bg);
+}
+
+.secondary-button:hover {
+ background-color: var(--button-secondary-hover-bg);
+}
+
+.danger-button {
+ background-color: var(--button-danger-bg);
+}
+
+.danger-button:hover {
+ background-color: var(--button-danger-hover-bg);
+}
+
+.logout-button {
+ background-color: #f44336;
+}
+.logout-button:hover {
+ background-color: #d32f2f;
+}
+
+.actions-bar {
+ display: flex;
+ justify-content: flex-start; /* Alignement à gauche */
+ gap: 10px; /* Espace entre les boutons */
+ margin-bottom: 20px;
+ flex-wrap: wrap; /* Pour la réactivité sur mobile */
+}
+
+/* Formulaires */
+form label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: bold;
+}
+
+form input[type="text"],
+form input[type="password"],
+form select {
+ width: calc(100% - 22px); /* 100% moins padding et border */
+ padding: 10px;
+ margin-bottom: 15px;
+ border: 1px solid var(--input-border);
+ border-radius: 8px; /* Coins arrondis */
+ font-size: 1em;
+ box-sizing: border-box; /* Inclut padding et border dans la largeur */
+}
+
+form input[type="text"]:focus,
+form input[type="password"]:focus,
+form select:focus {
+ border-color: var(--input-focus-border);
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
+}
+
+form button[type="submit"] {
+ width: auto;
+ padding: 12px 25px;
+ font-size: 1.1em;
+ border-radius: 8px;
+}
+
+/* Messages de statut de certificat */
+.status-revoked {
+ color: var(--status-revoked-color);
+ font-weight: bold;
+}
+
+.status-active {
+ color: var(--status-active-color);
+ font-weight: bold;
+}
+
+/* Ligne de certificat révoqué */
+tr.revoked-cert {
+ text-decoration: line-through;
+ color: #888;
+ background-color: #fce4e4; /* Léger fond rouge pour le mode clair */
+}
+tr.revoked-cert:hover {
+ background-color: #fcc;
+}
+
+/* Pied de page de l'application */
+.app-footer {
+ background-color: #333;
+ color: #f4f4f4;
+ text-align: center;
+ padding: 15px 0;
+ margin-top: auto; /* Pousse le footer en bas */
+}
+
+.app-footer .container {
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+ background-color: transparent; /* Pas de fond blanc pour le footer */
+ box-shadow: none;
+}
+
+/* Réactivité mobile */
+@media (max-width: 768px) {
+ .container {
+ margin: 10px;
+ padding: 15px;
+ }
+
+ .app-header .header-content {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ nav ul {
+ flex-direction: column;
+ gap: 5px;
+ }
+
+ nav ul li a {
+ padding: 10px 15px;
+ text-align: center;
+ }
+
+ .actions-bar {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .button {
+ width: 100%;
+ margin-bottom: 10px;
+ }
+
+ form input[type="text"],
+ form input[type="password"],
+ form select {
+ width: 100%;
+ }
+}
diff --git a/app/public/dark-mode.css b/app/public/dark-mode.css
new file mode 100644
index 0000000..7f5a78e
--- /dev/null
+++ b/app/public/dark-mode.css
@@ -0,0 +1,57 @@
+/* Styles pour le mode sombre */
+body.dark-mode {
+ --bg-color: #2c2c2c;
+ --text-color: #e0e0e0;
+ --container-bg: #3a3a3a;
+ --container-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
+ --header-bg: #1a4d7c;
+ --header-text: #f0f0f0;
+ --nav-bg: #444;
+ --nav-link-color: #9cb3cc;
+ --nav-link-hover-bg: #555;
+ --table-border-color: #555;
+ --table-header-bg: #1a4d7c;
+ --table-header-text: #fff;
+ --table-row-even-bg: #333;
+ --button-primary-bg: #0056b3;
+ --button-primary-hover-bg: #004085;
+ --button-secondary-bg: #5a6268;
+ --button-secondary-hover-bg: #43484f;
+ --button-danger-bg: #a71d2a;
+ --button-danger-hover-bg: #7a151f;
+ --status-revoked-color: #ff6666;
+ --status-active-color: #66ff66;
+ --message-success-color: #a3e6a3;
+ --message-error-color: #ff9999;
+ --input-border: #666;
+ --input-focus-border: #007bff;
+}
+
+body.dark-mode .app-footer {
+ background-color: #1a1a1a;
+ color: #ccc;
+}
+
+body.dark-mode tr.revoked-cert {
+ background-color: #5c2c2c; /* Fond plus foncé pour le mode sombre */
+ color: #aaa;
+}
+body.dark-mode tr.revoked-cert:hover {
+ background-color: #703c3c;
+}
+
+/* Spécifiques pour les inputs en mode sombre */
+body.dark-mode form input[type="text"],
+body.dark-mode form input[type="password"],
+body.dark-mode form select {
+ background-color: #4a4a4a;
+ color: var(--text-color);
+ border: 1px solid var(--input-border);
+}
+
+body.dark-mode form input[type="text"]:focus,
+body.dark-mode form input[type="password"]:focus,
+body.dark-mode form select:focus {
+ border-color: var(--input-focus-border);
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.4);
+}
diff --git a/app/public/index.php b/app/public/index.php
new file mode 100644
index 0000000..ed558e5
--- /dev/null
+++ b/app/public/index.php
@@ -0,0 +1,162 @@
+getMessage());
+ die("Une erreur est survenue lors de la connexion à la base de données. Veuillez réessayer plus tard.");
+}
+
+// Initialisation des services principaux
+$dbInstance = Database::getInstance(); // Récupère l'instance PDO
+$authService = new AuthService($dbInstance);
+$logService = new LogService(APP_LOG_PATH);
+$langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+
+// ----------------------------------------------------
+// Gestion de la Langue et du Mode Sombre via URL ou Session
+// ----------------------------------------------------
+
+// Traitement du changement de langue
+if (isset($_GET['lang'])) {
+ $langService->setLanguage($_GET['lang']);
+ // Redirige pour nettoyer le paramètre GET de l'URL
+ header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
+ exit();
+}
+$currentLang = $langService->getLanguage();
+$translations = $langService->getTranslations(); // Charge les traductions pour la langue actuelle
+
+// Traitement du mode sombre
+DarkMode::init(); // Initialise le mode sombre si ce n'est pas déjà fait
+if (isset($_GET['dark_mode'])) {
+ DarkMode::toggle($_GET['dark_mode']);
+ // Redirige pour nettoyer le paramètre GET de l'URL
+ header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
+ exit();
+}
+
+// ----------------------------------------------------
+// Log de chaque requête entrante (pour le débogage/audit)
+// ----------------------------------------------------
+$logService->log('info', 'Requête reçue: ' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'], $authService->getUserId(), $_SERVER['REMOTE_ADDR']);
+
+
+// ----------------------------------------------------
+// Processus d'initialisation de l'application au premier lancement
+// Crée le Root CA et le premier compte administrateur si non existants.
+// ----------------------------------------------------
+
+$stmt = $dbInstance->query("SELECT COUNT(*) FROM users");
+$userCount = $stmt->fetchColumn();
+
+// Vérifier l'existence du certificat root
+$rootCertExists = file_exists(ROOT_CA_PATH . '/certs/ca.cert.pem');
+
+if ($userCount === 0 || !$rootCertExists) {
+ // Afficher une page d'initialisation ou un message d'attente
+ echo "
Initialisation";
+ echo "
Initialisation de l'Application
";
+ echo "
Ceci est le premier lancement. Nous allons configurer la base de données, créer le certificat Root CA et le premier compte administrateur.
";
+
+ // Création du certificat Root CA si non existant
+ if (!$rootCertExists) {
+ echo "
Création du certificat Root CA en cours...
";
+ $logService->log('info', 'Lancement de la création du certificat Root CA.', null, $_SERVER['REMOTE_ADDR']);
+ // Exécution du script shell de création de certificat root
+ $command = escapeshellcmd(SCRIPTS_PATH . '/create_root_cert.sh'); // Utilise la constante
+ $output = shell_exec($command . ' 2>&1');
+ $logService->log('info', "Résultat création Root CA: " . $output, null, $_SERVER['REMOTE_ADDR']);
+
+ if (file_exists(ROOT_CA_PATH . '/certs/ca.cert.pem')) {
+ echo "
Certificat Root CA créé avec succès.
";
+ // Extraire la date d'expiration du certificat créé pour l'enregistrer en BDD
+ $certInfo = shell_exec("openssl x509 -in " . escapeshellarg(ROOT_CA_PATH . '/certs/ca.cert.pem') . " -noout -enddate 2>/dev/null | cut -d= -f2");
+ $expirationTimestamp = strtotime($certInfo);
+ $expirationDate = $expirationTimestamp ? date('Y-m-d H:i:s', $expirationTimestamp) : (new DateTime('+10 years'))->format('Y-m-d H:i:s');
+
+ // Enregistrer le certificat root dans la base de données
+ $stmt = $dbInstance->prepare("INSERT INTO certificates (name, type, expiration_date) VALUES (?, ?, ?)");
+ $stmt->execute(['ca.cert.pem', 'root', $expirationDate]);
+ } else {
+ echo "
Erreur lors de la création du certificat Root CA. Veuillez vérifier les logs PHP et Docker.
";
+ echo "
" . htmlspecialchars($output) . "
";
+ // Arrête l'exécution pour que l'utilisateur puisse voir l'erreur
+ exit();
+ }
+ }
+
+ // Création du premier compte administrateur si non existant
+ if ($userCount === 0) {
+ echo "
Création du premier compte administrateur...
";
+ $adminUsername = 'admin';
+ $adminPasswordPlain = 'adminpass'; // Mot de passe par défaut très faible, À CHANGER IMMÉDIATEMENT EN PRODUCTION !
+ $adminPasswordHashed = password_hash($adminPasswordPlain, PASSWORD_DEFAULT);
+
+ $stmt = $dbInstance->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
+ $stmt->execute([$adminUsername, $adminPasswordHashed, 'admin']);
+ $logService->log('info', "Compte administrateur '$adminUsername' créé.", $stmt->lastInsertId(), $_SERVER['REMOTE_ADDR']);
+ echo "
Compte administrateur 'admin' créé avec succès. Mot de passe initial: {$adminPasswordPlain} (veuillez le changer après la première connexion !)
";
+ }
+
+ echo "
Initialisation terminée. Redirection vers la page de connexion dans 5 secondes...
";
+ echo "
";
+ // Redirection automatique après l'initialisation
+ header('Refresh: 5; URL=/login');
+ exit();
+}
+// ----------------------------------------------------
+// Fin du processus d'initialisation
+// ----------------------------------------------------
+
+
+// ----------------------------------------------------
+// Configuration du routeur de l'application
+// ----------------------------------------------------
+$router = new Router();
+
+// Routes publiques (accessibles sans authentification)
+$router->addRoute('GET', '/', 'HomeController@index');
+$router->addRoute('GET', '/login', 'AuthController@showLoginForm');
+$router->addRoute('POST', '/login', 'AuthController@login');
+
+// Routes protégées (nécessitent une authentification)
+// Le dernier paramètre 'true' indique que la route nécessite une authentification
+$router->addRoute('GET', '/dashboard', 'DashboardController@index', true);
+$router->addRoute('GET', '/certificates', 'CertificateController@index', true);
+$router->addRoute('GET', '/certificates/create', 'CertificateController@showCreateForm', true);
+$router->addRoute('POST', '/certificates/create', 'CertificateController@create', true);
+$router->addRoute('POST', '/certificates/revoke', 'CertificateController@revoke', true);
+$router->addRoute('GET', '/perimeters', 'PerimeterController@index', true);
+$router->addRoute('GET', '/perimeters/create', 'PerimeterController@showCreateForm', true);
+$router->addRoute('POST', '/perimeters/create', 'PerimeterController@create', true);
+$router->addRoute('GET', '/users', 'UserController@index', true);
+$router->addRoute('GET', '/users/create', 'UserController@showCreateForm', true);
+$router->addRoute('POST', '/users/create', 'UserController@create', true);
+$router->addRoute('POST', '/users/delete', 'UserController@delete', true);
+$router->addRoute('GET', '/logout', 'AuthController@logout', true);
+
+// Exécuter le routage
+$router->dispatch();
diff --git a/app/public/index.php.bak b/app/public/index.php.bak
new file mode 100644
index 0000000..e89fe8f
--- /dev/null
+++ b/app/public/index.php.bak
@@ -0,0 +1,162 @@
+getMessage());
+ die("Une erreur est survenue lors de la connexion à la base de données. Veuillez réessayer plus tard.");
+}
+
+// Initialisation des services principaux
+$dbInstance = Database::getInstance(); // Récupère l'instance PDO
+$authService = new AuthService($dbInstance);
+$logService = new LogService(APP_LOG_PATH);
+$langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+
+// ----------------------------------------------------
+// Gestion de la Langue et du Mode Sombre via URL ou Session
+// ----------------------------------------------------
+
+// Traitement du changement de langue
+if (isset($_GET['lang'])) {
+ $langService->setLanguage($_GET['lang']);
+ // Redirige pour nettoyer le paramètre GET de l'URL
+ header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
+ exit();
+}
+$currentLang = $langService->getLanguage();
+$translations = $langService->getTranslations(); // Charge les traductions pour la langue actuelle
+
+// Traitement du mode sombre
+DarkMode::init(); // Initialise le mode sombre si ce n'est pas déjà fait
+if (isset($_GET['dark_mode'])) {
+ DarkMode::toggle($_GET['dark_mode']);
+ // Redirige pour nettoyer le paramètre GET de l'URL
+ header('Location: ' . strtok($_SERVER['REQUEST_URI'], '?'));
+ exit();
+}
+
+// ----------------------------------------------------
+// Log de chaque requête entrante (pour le débogage/audit)
+// ----------------------------------------------------
+$logService->log('info', 'Requête reçue: ' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'], $authService->getUserId(), $_SERVER['REMOTE_ADDR']);
+
+
+// ----------------------------------------------------
+// Processus d'initialisation de l'application au premier lancement
+// Crée le Root CA et le premier compte administrateur si non existants.
+// ----------------------------------------------------
+
+$stmt = $dbInstance->query("SELECT COUNT(*) FROM users");
+$userCount = $stmt->fetchColumn();
+
+// Vérifier l'existence du certificat root
+$rootCertExists = file_exists(ROOT_CA_PATH . '/certs/ca.cert.pem');
+
+if ($userCount === 0 || !$rootCertExists) {
+ // Afficher une page d'initialisation ou un message d'attente
+ echo "Initialisation";
+ echo "
Initialisation de l'Application
";
+ echo "
Ceci est le premier lancement. Nous allons configurer la base de données, créer le certificat Root CA et le premier compte administrateur.
";
+
+ // Création du certificat Root CA si non existant
+ if (!$rootCertExists) {
+ echo "
Création du certificat Root CA en cours...
";
+ $logService->log('info', 'Lancement de la création du certificat Root CA.', null, $_SERVER['REMOTE_ADDR']);
+ // Exécution du script shell de création de certificat root
+ $command = escapeshellcmd(SCRIPTS_PATH . '/create_root_cert.sh'); // Utilise la constante
+ $output = shell_exec($command . ' 2>&1');
+ $logService->log('info', "Résultat création Root CA: " . $output, null, $_SERVER['REMOTE_ADDR']);
+
+ if (file_exists(ROOT_CA_PATH . '/certs/ca.cert.pem')) {
+ echo "
Certificat Root CA créé avec succès.
";
+ // Extraire la date d'expiration du certificat créé pour l'enregistrer en BDD
+ $certInfo = shell_exec("openssl x509 -in " . escapeshellarg(ROOT_CA_PATH . '/certs/ca.cert.pem') . " -noout -enddate 2>/dev/null | cut -d= -f2");
+ $expirationTimestamp = strtotime($certInfo);
+ $expirationDate = $expirationTimestamp ? date('Y-m-d H:i:s', $expirationTimestamp) : (new DateTime('+10 years'))->format('Y-m-d H:i:s');
+
+ // Enregistrer le certificat root dans la base de données
+ $stmt = $dbInstance->prepare("INSERT INTO certificates (name, type, expiration_date) VALUES (?, ?, ?)");
+ $stmt->execute(['ca.cert.pem', 'root', $expirationDate]);
+ } else {
+ echo "
Erreur lors de la création du certificat Root CA. Veuillez vérifier les logs PHP et Docker.
";
+ echo "
" . htmlspecialchars($output) . "
";
+ // Arrête l'exécution pour que l'utilisateur puisse voir l'erreur
+ exit();
+ }
+ }
+
+ // Création du premier compte administrateur si non existant
+ if ($userCount === 0) {
+ echo "
Création du premier compte administrateur...
";
+ $adminUsername = 'admin';
+ $adminPasswordPlain = 'adminpass'; // Mot de passe par défaut très faible, À CHANGER IMMÉDIATEMENT EN PRODUCTION !
+ $adminPasswordHashed = password_hash($adminPasswordPlain, PASSWORD_DEFAULT);
+
+ $stmt = $dbInstance->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
+ $stmt->execute([$adminUsername, $adminPasswordHashed, 'admin']);
+ $logService->log('info', "Compte administrateur '$adminUsername' créé.", $stmt->lastInsertId(), $_SERVER['REMOTE_ADDR']);
+ echo "
Compte administrateur 'admin' créé avec succès. Mot de passe initial: {$adminPasswordPlain} (veuillez le changer après la première connexion !)
";
+ }
+
+ echo "
Initialisation terminée. Redirection vers la page de connexion dans 5 secondes...
";
+ echo "
";
+ // Redirection automatique après l'initialisation
+ header('Refresh: 5; URL=/login');
+ exit();
+}
+// ----------------------------------------------------
+// Fin du processus d'initialisation
+// ----------------------------------------------------
+
+
+// ----------------------------------------------------
+// Configuration du routeur de l'application
+// ----------------------------------------------------
+$router = new Router();
+
+// Routes publiques (accessibles sans authentification)
+$router->addRoute('GET', '/', 'HomeController@index');
+$router->addRoute('GET', '/login', 'AuthController@showLoginForm');
+$router->addRoute('POST', '/login', 'AuthController@login');
+
+// Routes protégées (nécessitent une authentification)
+// Le dernier paramètre 'true' indique que la route nécessite une authentification
+$router->addRoute('GET', '/dashboard', 'DashboardController@index', true);
+$router->addRoute('GET', '/certificates', 'CertificateController@index', true);
+$router->addRoute('GET', '/certificates/create', 'CertificateController@showCreateForm', true);
+$router->addRoute('POST', '/certificates/create', 'CertificateController@create', true);
+$router->addRoute('POST', '/certificates/revoke', 'CertificateController@revoke', true);
+$router->addRoute('GET', '/perimeters', 'PerimeterController@index', true);
+$router->addRoute('GET', '/perimeters/create', 'PerimeterController@showCreateForm', true);
+$router->addRoute('POST', '/perimeters/create', 'PerimeterController@create', true);
+$router->addRoute('GET', '/users', 'UserController@index', true);
+$router->addRoute('GET', '/users/create', 'UserController@showCreateForm', true);
+$router->addRoute('POST', '/users/create', 'UserController@create', true);
+$router->addRoute('POST', '/users/delete', 'UserController@delete', true);
+$router->addRoute('GET', '/logout', 'AuthController@logout', true); # CORRIGÉ: de AuthController@@logout à AuthController@logout
+
+// Exécuter le routage
+$router->dispatch();
diff --git a/app/public/ocsp_responder.php b/app/public/ocsp_responder.php
new file mode 100644
index 0000000..5ad3363
--- /dev/null
+++ b/app/public/ocsp_responder.php
@@ -0,0 +1,47 @@
+getMessage());
+ http_response_code(500);
+ die("OCSP service temporarily unavailable.");
+}
+
+$dbInstance = Database::getInstance();
+$authService = new AuthService($dbInstance); // Peut être utilisé pour logguer des requêtes OCSP anonymes
+$logService = new LogService(APP_LOG_PATH);
+
+
+// La logique OCSP réelle serait plus complexe.
+// Ce script attendrait une requête POST OCSP (application/ocsp-request)
+// et appellerait le contrôleur OCSP pour la traiter.
+
+// Pour un POC, nous allons simplement appeler la méthode du contrôleur dédiée.
+// En production, Nginx redirigerait une requête HTTP POST spécifique vers ce script.
+// Le client OCSP enverrait une requête binaire.
+// Pour l'instant, ce script est juste un point d'entrée.
+
+$controller = new OcspController();
+$controller->handleRequest();
+
+// Log la requête OCSP
+$logService->log('info', 'Requête OCSP reçue.', null, $_SERVER['REMOTE_ADDR']);
diff --git a/app/public/ocsp_responder.php.bak b/app/public/ocsp_responder.php.bak
new file mode 100644
index 0000000..5ad3363
--- /dev/null
+++ b/app/public/ocsp_responder.php.bak
@@ -0,0 +1,47 @@
+getMessage());
+ http_response_code(500);
+ die("OCSP service temporarily unavailable.");
+}
+
+$dbInstance = Database::getInstance();
+$authService = new AuthService($dbInstance); // Peut être utilisé pour logguer des requêtes OCSP anonymes
+$logService = new LogService(APP_LOG_PATH);
+
+
+// La logique OCSP réelle serait plus complexe.
+// Ce script attendrait une requête POST OCSP (application/ocsp-request)
+// et appellerait le contrôleur OCSP pour la traiter.
+
+// Pour un POC, nous allons simplement appeler la méthode du contrôleur dédiée.
+// En production, Nginx redirigerait une requête HTTP POST spécifique vers ce script.
+// Le client OCSP enverrait une requête binaire.
+// Pour l'instant, ce script est juste un point d'entrée.
+
+$controller = new OcspController();
+$controller->handleRequest();
+
+// Log la requête OCSP
+$logService->log('info', 'Requête OCSP reçue.', null, $_SERVER['REMOTE_ADDR']);
diff --git a/app/public/style.css b/app/public/style.css
new file mode 100644
index 0000000..976be67
--- /dev/null
+++ b/app/public/style.css
@@ -0,0 +1,366 @@
+/* Variables CSS pour faciliter le basculement entre les thèmes */
+:root {
+ --bg-color: #f4f4f4;
+ --text-color: #333;
+ --container-bg: #fff;
+ --container-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ --header-bg: #0056b3;
+ --header-text: #fff;
+ --nav-bg: #e2e2e2;
+ --nav-link-color: #0056b3;
+ --nav-link-hover-bg: #d1d1d1;
+ --table-border-color: #ddd;
+ --table-header-bg: #0056b3;
+ --table-header-text: #fff;
+ --table-row-even-bg: #f2f2f2;
+ --button-primary-bg: #007bff;
+ --button-primary-hover-bg: #0056b3;
+ --button-secondary-bg: #6c757d;
+ --button-secondary-hover-bg: #5a6268;
+ --button-danger-bg: #dc3545;
+ --button-danger-hover-bg: #bd2130;
+ --status-revoked-color: red;
+ --status-active-color: green;
+ --message-success-color: green;
+ --message-error-color: red;
+ --input-border: #ccc;
+ --input-focus-border: #007bff;
+}
+
+/* Styles généraux */
+body {
+ font-family: 'Inter', sans-serif; /* Utilisation de Inter comme demandé */
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ line-height: 1.6;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.container {
+ max-width: 960px;
+ margin: 20px auto;
+ background-color: var(--container-bg);
+ padding: 20px 30px;
+ border-radius: 12px; /* Coins arrondis */
+ box-shadow: var(--container-shadow);
+ flex-grow: 1; /* Permet au container de prendre de l'espace */
+}
+
+h1, h2 {
+ color: var(--header-bg);
+ margin-top: 0;
+ margin-bottom: 20px;
+}
+
+/* En-tête de l'application */
+.app-header {
+ background-color: var(--header-bg);
+ color: var(--header-text);
+ padding: 15px 0;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+.app-title h1 {
+ color: var(--header-text);
+ margin: 0;
+ font-size: 1.8em;
+}
+
+.header-controls {
+ display: flex;
+ gap: 15px;
+}
+
+.language-switcher, .dark-mode-switcher {
+ display: flex;
+ align-items: center;
+}
+
+.lang-button, .dark-mode-button {
+ padding: 8px 12px;
+ text-decoration: none;
+ color: var(--header-text);
+ border: 1px solid rgba(255, 255, 255, 0.4);
+ border-radius: 8px; /* Coins arrondis */
+ transition: background-color 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.lang-button:hover, .dark-mode-button:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+}
+
+.lang-button.active {
+ background-color: rgba(255, 255, 255, 0.3);
+ border-color: var(--header-text);
+}
+
+/* Styles de navigation */
+nav ul {
+ list-style: none;
+ padding: 0;
+ margin: 20px 0;
+ background-color: var(--nav-bg);
+ border-radius: 10px; /* Coins arrondis */
+ display: flex;
+ justify-content: center;
+ overflow: hidden; /* Pour que les coins arrondis fonctionnent bien avec le hover */
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
+}
+
+nav ul li {
+ margin: 0;
+}
+
+nav ul li a {
+ display: block;
+ padding: 12px 20px;
+ text-decoration: none;
+ color: var(--nav-link-color);
+ font-weight: bold;
+ transition: background-color 0.3s ease, color 0.3s ease;
+ border-radius: 8px; /* Appliquer aux liens pour le hover */
+}
+
+nav ul li a:hover {
+ background-color: var(--nav-link-hover-bg);
+ color: var(--button-primary-hover-bg); /* Ou une autre couleur contrastante */
+}
+
+/* Styles des messages */
+.success-message {
+ background-color: #d4edda;
+ color: var(--message-success-color);
+ border: 1px solid #c3e6cb;
+ padding: 10px;
+ border-radius: 8px;
+ margin-bottom: 15px;
+}
+
+.error-message {
+ background-color: #f8d7da;
+ color: var(--message-error-color);
+ border: 1px solid #f5c6cb;
+ padding: 10px;
+ border-radius: 8px;
+ margin-bottom: 15px;
+}
+
+/* Styles de tableau */
+.table-responsive {
+ overflow-x: auto; /* Pour les petits écrans */
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 20px;
+ border-radius: 12px; /* Coins arrondis pour le tableau */
+ overflow: hidden; /* Important pour que les coins arrondis soient visibles */
+}
+
+table, th, td {
+ border: 1px solid var(--table-border-color);
+}
+
+th, td {
+ padding: 12px 15px;
+ text-align: left;
+}
+
+th {
+ background-color: var(--table-header-bg);
+ color: var(--table-header-text);
+ font-weight: bold;
+}
+
+tr:nth-child(even) {
+ background-color: var(--table-row-even-bg);
+}
+
+tr:hover {
+ background-color: rgba(0, 0, 0, 0.05); /* Léger survol */
+}
+
+/* Styles des boutons */
+.button {
+ display: inline-block;
+ padding: 10px 20px;
+ font-size: 1em;
+ text-decoration: none;
+ color: white;
+ border-radius: 8px; /* Coins arrondis */
+ transition: background-color 0.3s ease, transform 0.2s ease;
+ border: none;
+ cursor: pointer;
+ text-align: center;
+}
+
+.button:active {
+ transform: translateY(1px);
+}
+
+.primary-button {
+ background-color: var(--button-primary-bg);
+}
+
+.primary-button:hover {
+ background-color: var(--button-primary-hover-bg);
+}
+
+.secondary-button {
+ background-color: var(--button-secondary-bg);
+}
+
+.secondary-button:hover {
+ background-color: var(--button-secondary-hover-bg);
+}
+
+.danger-button {
+ background-color: var(--button-danger-bg);
+}
+
+.danger-button:hover {
+ background-color: var(--button-danger-hover-bg);
+}
+
+.logout-button {
+ background-color: #f44336;
+}
+.logout-button:hover {
+ background-color: #d32f2f;
+}
+
+.actions-bar {
+ display: flex;
+ justify-content: flex-start; /* Alignement à gauche */
+ gap: 10px; /* Espace entre les boutons */
+ margin-bottom: 20px;
+ flex-wrap: wrap; /* Pour la réactivité sur mobile */
+}
+
+/* Formulaires */
+form label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: bold;
+}
+
+form input[type="text"],
+form input[type="password"],
+form select {
+ width: calc(100% - 22px); /* 100% moins padding et border */
+ padding: 10px;
+ margin-bottom: 15px;
+ border: 1px solid var(--input-border);
+ border-radius: 8px; /* Coins arrondis */
+ font-size: 1em;
+ box-sizing: border-box; /* Inclut padding et border dans la largeur */
+}
+
+form input[type="text"]:focus,
+form input[type="password"]:focus,
+form select:focus {
+ border-color: var(--input-focus-border);
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
+}
+
+form button[type="submit"] {
+ width: auto;
+ padding: 12px 25px;
+ font-size: 1.1em;
+ border-radius: 8px;
+}
+
+/* Messages de statut de certificat */
+.status-revoked {
+ color: var(--status-revoked-color);
+ font-weight: bold;
+}
+
+.status-active {
+ color: var(--status-active-color);
+ font-weight: bold;
+}
+
+/* Ligne de certificat révoqué */
+tr.revoked-cert {
+ text-decoration: line-through;
+ color: #888;
+ background-color: #fce4e4; /* Léger fond rouge pour le mode clair */
+}
+tr.revoked-cert:hover {
+ background-color: #fcc;
+}
+
+/* Pied de page de l'application */
+.app-footer {
+ background-color: #333;
+ color: #f4f4f4;
+ text-align: center;
+ padding: 15px 0;
+ margin-top: auto; /* Pousse le footer en bas */
+}
+
+.app-footer .container {
+ max-width: 960px;
+ margin: 0 auto;
+ padding: 0 20px;
+ background-color: transparent; /* Pas de fond blanc pour le footer */
+ box-shadow: none;
+}
+
+/* Réactivité mobile */
+@media (max-width: 768px) {
+ .container {
+ margin: 10px;
+ padding: 15px;
+ }
+
+ .app-header .header-content {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ nav ul {
+ flex-direction: column;
+ gap: 5px;
+ }
+
+ nav ul li a {
+ padding: 10px 15px;
+ text-align: center;
+ }
+
+ .actions-bar {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .button {
+ width: 100%;
+ margin-bottom: 10px;
+ }
+
+ form input[type="text"],
+ form input[type="password"],
+ form select {
+ width: 100%;
+ }
+}
diff --git a/app/src/Controllers/AuthController.php b/app/src/Controllers/AuthController.php
new file mode 100644
index 0000000..3fb0d2d
--- /dev/null
+++ b/app/src/Controllers/AuthController.php
@@ -0,0 +1,98 @@
+authService = new AuthService($db);
+ $this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+ $this->logService = new LogService(APP_LOG_PATH);
+ }
+
+ /**
+ * Affiche le formulaire de connexion.
+ * Redirige vers le tableau de bord si l'utilisateur est déjà connecté.
+ */
+ public function showLoginForm()
+ {
+ if ($this->authService->isLoggedIn()) {
+ header('Location: /dashboard');
+ exit();
+ }
+
+ // Charge les traductions et la classe pour le mode sombre pour la vue
+ global $translations; // Utilise la variable globale chargée dans index.php
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+
+ // Récupère les messages d'erreur/succès de la session
+ $error = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']); // Supprime le message après l'avoir affiché
+
+ require_once APP_ROOT_DIR . '/src/Views/auth/login.php';
+ }
+
+ /**
+ * Traite la soumission du formulaire de connexion.
+ */
+ public function login()
+ {
+ // Vérifie si la requête est bien un POST
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ header('Location: /login');
+ exit();
+ }
+
+ $username = trim($_POST['username'] ?? '');
+ $password = $_POST['password'] ?? '';
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+
+ // Validation simple des entrées
+ if (empty($username) || empty($password)) {
+ $_SESSION['error'] = $this->langService->__('login_error_empty_fields');
+ header('Location: /login');
+ exit();
+ }
+
+ if ($this->authService->login($username, $password, $ipAddress)) {
+ // Connexion réussie
+ header('Location: /dashboard');
+ exit();
+ } else {
+ // Connexion échouée
+ $_SESSION['error'] = $this->langService->__('login_error_credentials');
+ header('Location: /login');
+ exit();
+ }
+ }
+
+ /**
+ * Déconnecte l'utilisateur.
+ */
+ public function logout()
+ {
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $this->authService->logout($ipAddress);
+ header('Location: /login');
+ exit();
+ }
+}
diff --git a/app/src/Controllers/CertificateController.php b/app/src/Controllers/CertificateController.php
new file mode 100644
index 0000000..bd1d6d6
--- /dev/null
+++ b/app/src/Controllers/CertificateController.php
@@ -0,0 +1,251 @@
+db = Database::getInstance();
+ $this->authService = new AuthService($this->db);
+ $this->logService = new LogService(APP_LOG_PATH);
+ $this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+ }
+
+ /**
+ * Affiche la liste des certificats, regroupés par périmètre fonctionnel.
+ */
+ public function index()
+ {
+ if (!$this->authService->isLoggedIn()) {
+ header('Location: /login');
+ exit();
+ }
+
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+ $userRole = $this->authService->getUserRole();
+
+ // Récupérer les périmètres et les certificats
+ // Joindre pour obtenir le nom du périmètre
+ $certificates = $this->db->query("
+ SELECT
+ c.id, c.name, c.type, c.expiration_date, c.is_revoked, c.revoked_at,
+ fp.name as perimeter_name
+ FROM
+ certificates c
+ LEFT JOIN
+ functional_perimeters fp ON c.functional_perimeter_id = fp.id
+ ORDER BY
+ fp.name IS NULL DESC, fp.name ASC, c.type DESC, c.expiration_date DESC
+ ")->fetchAll();
+
+ // Regrouper les certificats par périmètre fonctionnel
+ $groupedCertificates = [];
+ foreach ($certificates as $cert) {
+ $perimeterName = $cert['perimeter_name'] ?? 'Certificats Root'; // Nom pour le groupe Root
+ if (!isset($groupedCertificates[$perimeterName])) {
+ $groupedCertificates[$perimeterName] = [];
+ }
+ $groupedCertificates[$perimeterName][] = $cert;
+ }
+
+ $successMessage = $_SESSION['success'] ?? null;
+ unset($_SESSION['success']);
+ $errorMessage = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']);
+
+ require_once APP_ROOT_DIR . '/src/Views/certificates/index.php';
+ }
+
+ /**
+ * Affiche le formulaire de création d'un nouveau certificat.
+ */
+ public function showCreateForm()
+ {
+ if (!$this->authService->isLoggedIn()) {
+ header('Location: /login');
+ exit();
+ }
+
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+
+ // Récupérer la liste des périmètres fonctionnels pour le sélecteur
+ $perimeters = $this->db->query("SELECT id, name FROM functional_perimeters ORDER BY name ASC")->fetchAll();
+
+ $errorMessage = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']);
+
+ require_once APP_ROOT_DIR . '/src/Views/certificates/create.php';
+ }
+
+ /**
+ * Traite la soumission du formulaire de création de certificat.
+ */
+ public function create()
+ {
+ if (!$this->authService->isLoggedIn() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
+ header('Location: /login');
+ exit();
+ }
+
+ $subdomainName = trim($_POST['subdomain_name'] ?? '');
+ $functionalPerimeterId = $_POST['functional_perimeter_id'] ?? null;
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $userId = $this->authService->getUserId();
+
+ if (empty($subdomainName) || empty($functionalPerimeterId)) {
+ $_SESSION['error'] = $this->langService->__('cert_create_error_empty_fields');
+ header('Location: /certificates/create');
+ exit();
+ }
+
+ // Récupérer le nom du périmètre fonctionnel pour le script shell
+ $stmt = $this->db->prepare("SELECT name FROM functional_perimeters WHERE id = ?");
+ $stmt->execute([$functionalPerimeterId]);
+ $perimeter = $stmt->fetch();
+
+ if (!$perimeter) {
+ $_SESSION['error'] = $this->langService->__('cert_create_error_perimeter_not_found');
+ header('Location: /certificates/create');
+ exit();
+ }
+ $functionalPerimeterName = $perimeter['name'];
+
+ // Préparer la commande du script shell
+ // Important: utiliser escapeshellarg pour protéger les arguments
+ $command = escapeshellcmd(SCRIPTS_PATH . '/create_cert.sh') . ' ' .
+ escapeshellarg($subdomainName) . ' ' .
+ escapeshellarg($functionalPerimeterName);
+
+ $this->logService->log('info', "Tentative de création du certificat '$subdomainName' pour le périmètre '$functionalPerimeterName'. Commande: '$command'", $userId, $ipAddress);
+
+ // Exécuter le script shell
+ $output = shell_exec($command . ' 2>&1'); // Redirige stderr vers stdout
+
+ // Vérifier le résultat du script (simple vérification de chaîne, une meilleure parsage serait utile)
+ if (strpos($output, "Certificat '${subdomainName}.${functionalPerimeterName}.cert' créé avec succès") !== false) {
+ // Extraire la date d'expiration du certificat créé (en lisant le fichier cert ou en estimant 1 an)
+ $certFileName = "{$subdomainName}.{$functionalPerimeterName}.cert.pem";
+ $fullCertPath = INTERMEDIATE_CA_PATH_BASE . "/{$functionalPerimeterName}/certs/{$certFileName}";
+
+ $expirationDate = (new \DateTime('+1 year'))->format('Y-m-d H:i:s'); // Valeur par défaut
+ if (file_exists($fullCertPath)) {
+ $certInfo = shell_exec("openssl x509 -in " . escapeshellarg($fullCertPath) . " -noout -enddate 2>/dev/null | cut -d= -f2");
+ $expirationTimestamp = strtotime($certInfo);
+ if ($expirationTimestamp) {
+ $expirationDate = date('Y-m-d H:i:s', $expirationTimestamp);
+ }
+ }
+
+
+ // Enregistrer le certificat dans la base de données
+ $stmt = $this->db->prepare("INSERT INTO certificates (name, type, functional_perimeter_id, expiration_date) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$certFileName, 'simple', $functionalPerimeterId, $expirationDate]);
+
+ $this->logService->log('info', "Certificat '{$certFileName}' créé et enregistré pour le périmètre '{$functionalPerimeterName}'.", $userId, $ipAddress);
+ $_SESSION['success'] = $this->langService->__('cert_create_success');
+ } else {
+ $_SESSION['error'] = $this->langService->__('cert_create_error', ['output' => htmlspecialchars($output)]);
+ $this->logService->log('error', "Échec création certificat '$subdomainName' pour périmètre '$functionalPerimeterName'. Output: $output", $userId, $ipAddress);
+ }
+
+ header('Location: /certificates');
+ exit();
+ }
+
+ /**
+ * Traite la révocation d'un certificat.
+ */
+ public function revoke()
+ {
+ if (!$this->authService->isLoggedIn() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
+ header('Location: /login');
+ exit();
+ }
+
+ $certificateId = $_POST['certificate_id'] ?? null;
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $userId = $this->authService->getUserId();
+
+ if (empty($certificateId)) {
+ $_SESSION['error'] = $this->langService->__('cert_revoke_error_id_missing');
+ header('Location: /certificates');
+ exit();
+ }
+
+ // Récupérer les informations du certificat depuis la DB
+ $stmt = $this->db->prepare("SELECT c.name, c.type, fp.name as perimeter_name FROM certificates c LEFT JOIN functional_perimeters fp ON c.functional_perimeter_id = fp.id WHERE c.id = ?");
+ $stmt->execute([$certificateId]);
+ $cert = $stmt->fetch();
+
+ if (!$cert) {
+ $_SESSION['error'] = $this->langService->__('cert_revoke_error_not_found');
+ header('Location: /certificates');
+ exit();
+ }
+
+ // Empêcher la révocation des certificats Root ou Intermédiaires via l'interface
+ if ($cert['type'] === 'root' || $cert['type'] === 'intermediate') {
+ $_SESSION['error'] = $this->langService->__('cert_revoke_error_ca_revocation');
+ header('Location: /certificates');
+ exit();
+ }
+
+ // Préparer le nom de base du certificat pour le script (sans l'extension .pem)
+ $certBaseName = str_replace('.cert.pem', '.cert', $cert['name']);
+ $functionalPerimeterName = $cert['perimeter_name'];
+
+ // Vérifier si le certificat n'est pas déjà révoqué dans la DB
+ if ($cert['is_revoked']) {
+ $_SESSION['error'] = $this->langService->__('cert_revoke_error_already_revoked');
+ header('Location: /certificates');
+ exit();
+ }
+
+ // Appeler le script shell de révocation
+ $command = escapeshellcmd(SCRIPTS_PATH . '/revoke_cert.sh') . ' ' .
+ escapeshellarg($certBaseName) . ' ' .
+ escapeshellarg($functionalPerimeterName);
+
+ $this->logService->log('info', "Tentative de révocation du certificat '{$cert['name']}' pour le périmètre '$functionalPerimeterName'. Commande: '$command'", $userId, $ipAddress);
+
+ $output = shell_exec($command . ' 2>&1');
+
+ if (strpos($output, "Certificat '$certBaseName' révoqué avec succès.") !== false) {
+ // Mettre à jour le statut du certificat dans la base de données
+ $stmt = $this->db->prepare("UPDATE certificates SET is_revoked = TRUE, revoked_at = NOW() WHERE id = ?");
+ $stmt->execute([$certificateId]);
+
+ $this->logService->log('info', "Certificat '{$cert['name']}' révoqué et enregistré en DB.", $userId, $ipAddress);
+ $_SESSION['success'] = $this->langService->__('cert_revoke_success');
+ } else {
+ $_SESSION['error'] = $this->langService->__('cert_revoke_error', ['output' => htmlspecialchars($output)]);
+ $this->logService->log('error', "Échec révocation certificat '{$cert['name']}': $output", $userId, $ipAddress);
+ }
+
+ header('Location: /certificates');
+ exit();
+ }
+}
diff --git a/app/src/Controllers/DashboardController.php b/app/src/Controllers/DashboardController.php
new file mode 100644
index 0000000..8938e9a
--- /dev/null
+++ b/app/src/Controllers/DashboardController.php
@@ -0,0 +1,47 @@
+authService = new AuthService(Database::getInstance());
+ $this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+ }
+
+ /**
+ * Affiche le tableau de bord.
+ * Redirige vers la page de connexion si l'utilisateur n'est pas connecté.
+ */
+ public function index()
+ {
+ if (!$this->authService->isLoggedIn()) {
+ header('Location: /login');
+ exit();
+ }
+
+ // Récupère les traductions et les informations pour la vue
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $username = $this->authService->getUsername();
+ $darkModeClass = DarkMode::getBodyClass();
+ $userRole = $this->authService->getUserRole(); // Pour afficher/masquer certains éléments
+
+ require_once APP_ROOT_DIR . '/src/Views/dashboard/index.php';
+ }
+}
diff --git a/app/src/Controllers/HomeController.php b/app/src/Controllers/HomeController.php
new file mode 100644
index 0000000..019cf57
--- /dev/null
+++ b/app/src/Controllers/HomeController.php
@@ -0,0 +1,45 @@
+authService = new AuthService(Database::getInstance());
+ $this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+ }
+
+ /**
+ * Affiche la page d'accueil ou redirige.
+ */
+ public function index()
+ {
+ // Si l'utilisateur est déjà connecté, le rediriger vers le tableau de bord
+ if ($this->authService->isLoggedIn()) {
+ header('Location: /dashboard');
+ exit();
+ }
+
+ // Sinon, afficher la page de connexion
+ // On réutilise la logique de showLoginForm de AuthController pour éviter la duplication.
+ // On pourrait aussi faire un require de la vue directement.
+ $authController = new AuthController();
+ $authController->showLoginForm();
+ }
+}
diff --git a/app/src/Controllers/OcspController.php b/app/src/Controllers/OcspController.php
new file mode 100644
index 0000000..8a864ca
--- /dev/null
+++ b/app/src/Controllers/OcspController.php
@@ -0,0 +1,76 @@
+db = Database::getInstance();
+ $this->logService = new LogService(APP_LOG_PATH);
+ }
+
+ /**
+ * Gère les requêtes OCSP.
+ * En production, cette méthode devrait lire la requête OCSP binaire du corps de la requête HTTP,
+ * puis utiliser une bibliothèque ou un outil OpenSSL pour générer une réponse OCSP valide.
+ * Pour ce POC, nous allons juste logguer et retourner un message simple.
+ */
+ public function handleRequest()
+ {
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $requestMethod = $_SERVER['REQUEST_METHOD'];
+ $requestBody = file_get_contents('php://input'); // Récupère le corps de la requête (pour les requêtes POST OCSP)
+
+ $this->logService->log('info', "Requête OCSP reçue. Méthode: {$requestMethod}, Taille du corps: " . strlen($requestBody) . " octets.", null, $ipAddress);
+
+ // En-tête pour une réponse OCSP (Content-Type standard)
+ header('Content-Type: application/ocsp-response');
+ http_response_code(200); // OK
+
+ // --- Logique OCSP simplifiée pour POC ---
+ // En réalité, vous devriez:
+ // 1. Parser la requête OCSP ($requestBody) pour identifier le certificat à vérifier et son émetteur.
+ // 2. Chercher le statut de ce certificat dans votre base de données (table `certificates`).
+ // 3. Utiliser OpenSSL ou une bibliothèque PKI pour générer une réponse OCSP signée.
+ // Ceci impliquerait d'avoir le certificat et la clé du répondeur OCSP (souvent le CA intermédiaire lui-même)
+ // et l'index.txt et crl.pem du CA émetteur.
+ // 4. Envoyer la réponse binaire.
+
+ // Pour l'exemple, nous allons retourner une réponse factice ou une erreur.
+ // Une vraie réponse OCSP est un format binaire ASN.1.
+ // Retourner du texte est INCORRECT pour un client OCSP.
+ // Ceci est une SIMULATION pour le POC.
+
+ // Si la requête est un GET (pour les petites requêtes), le "cert" et l'"issuer" pourraient être dans les paramètres
+ // if ($requestMethod === 'GET' && isset($_GET['cert']) && isset($_GET['issuer'])) {
+ // $certHash = $_GET['cert'];
+ // $issuerHash = $_GET['issuer'];
+ // $this->logService->log('info', "Requête OCSP GET pour Cert: $certHash, Issuer: $issuerHash", null, $ipAddress);
+ // // Simuler une réponse "bon" ou "révoqué"
+ // echo "OCSP Response: Good (Simulated)";
+ // } else {
+ // echo "OCSP Responder (POC): Expects binary POST request or specific GET parameters.";
+ // }
+
+ // Retourner une réponse OCSP vide ou d'erreur (pour les clients qui s'attendent à du binaire)
+ // Un client OCSP s'attend à une réponse binaire, pas du texte.
+ // Pour éviter les erreurs chez le client OCSP, il vaut mieux renvoyer une réponse binaire valide
+ // ou au moins une réponse HTTP 500 pour indiquer un problème.
+ // Pour un POC minimaliste sans générer de binaire:
+ // C'est juste un marqueur de place. La vraie réponse serait générée par OpenSSL.
+ echo ""; // Réponse vide ou générer une vraie réponse binaire
+ }
+}
diff --git a/app/src/Controllers/PerimeterController.php b/app/src/Controllers/PerimeterController.php
new file mode 100644
index 0000000..6b1e7ce
--- /dev/null
+++ b/app/src/Controllers/PerimeterController.php
@@ -0,0 +1,147 @@
+db = Database::getInstance();
+ $this->authService = new AuthService($this->db);
+ $this->logService = new LogService(APP_LOG_PATH);
+ $this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+ }
+
+ /**
+ * Affiche la liste des périmètres fonctionnels.
+ */
+ public function index()
+ {
+ if (!$this->authService->isLoggedIn()) {
+ header('Location: /login');
+ exit();
+ }
+
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+
+ $perimeters = $this->db->query("SELECT * FROM functional_perimeters ORDER BY name ASC")->fetchAll();
+
+ $successMessage = $_SESSION['success'] ?? null;
+ unset($_SESSION['success']);
+ $errorMessage = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']);
+
+ require_once APP_ROOT_DIR . '/src/Views/perimeters/index.php';
+ }
+
+ /**
+ * Affiche le formulaire de création d'un nouveau périmètre fonctionnel.
+ */
+ public function showCreateForm()
+ {
+ if (!$this->authService->isLoggedIn() || $this->authService->getUserRole() !== 'admin') {
+ $_SESSION['error'] = $this->langService->__('permission_denied');
+ header('Location: /dashboard');
+ exit();
+ }
+
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+
+ $errorMessage = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']);
+
+ require_once APP_ROOT_DIR . '/src/Views/perimeters/create.php';
+ }
+
+ /**
+ * Traite la soumission du formulaire de création de périmètre fonctionnel.
+ */
+ public function create()
+ {
+ if (!$this->authService->isLoggedIn() || $this->authService->getUserRole() !== 'admin' || $_SERVER['REQUEST_METHOD'] !== 'POST') {
+ $_SESSION['error'] = $this->langService->__('permission_denied');
+ header('Location: /dashboard');
+ exit();
+ }
+
+ $perimeterName = trim($_POST['name'] ?? '');
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $userId = $this->authService->getUserId();
+
+ if (empty($perimeterName)) {
+ $_SESSION['error'] = $this->langService->__('perimeter_create_error_empty_name');
+ header('Location: /perimeters/create');
+ exit();
+ }
+
+ // Vérifier si le périmètre existe déjà
+ $stmt = $this->db->prepare("SELECT COUNT(*) FROM functional_perimeters WHERE name = ?");
+ $stmt->execute([$perimeterName]);
+ if ($stmt->fetchColumn() > 0) {
+ $_SESSION['error'] = $this->langService->__('perimeter_create_error_exists');
+ header('Location: /perimeters/create');
+ exit();
+ }
+
+ // Appeler le script shell pour créer le certificat intermédiaire
+ $command = escapeshellcmd(SCRIPTS_PATH . '/create_intermediate_cert.sh') . ' ' . escapeshellarg($perimeterName);
+
+ $this->logService->log('info', "Tentative de création du périmètre '$perimeterName' et de son certificat intermédiaire. Commande: '$command'", $userId, $ipAddress);
+
+ $output = shell_exec($command . ' 2>&1');
+
+ if (strpos($output, "Certificat Intermédiaire CA pour '$perimeterName' créé avec succès") !== false) {
+ // Enregistrer le périmètre dans la base de données
+ $stmt = $this->db->prepare("INSERT INTO functional_perimeters (name, intermediate_cert_name) VALUES (?, ?)");
+ $intermediateCertFileName = "intermediate.cert.pem"; // Nom générique du fichier pour l'intermédiaire
+ $stmt->execute([$perimeterName, $intermediateCertFileName]);
+ $perimeterId = $this->db->lastInsertId();
+
+ // Enregistrer le certificat intermédiaire dans la table des certificats
+ $fullCertPath = INTERMEDIATE_CA_PATH_BASE . "/{$perimeterName}/certs/intermediate.cert.pem";
+ $expirationDate = (new \DateTime('+5 years'))->format('Y-m-d H:i:s'); // Valeur par défaut
+ if (file_exists($fullCertPath)) {
+ $certInfo = shell_exec("openssl x509 -in " . escapeshellarg($fullCertPath) . " -noout -enddate 2>/dev/null | cut -d= -f2");
+ $expirationTimestamp = strtotime($certInfo);
+ if ($expirationTimestamp) {
+ $expirationDate = date('Y-m-d H:i:s', $expirationTimestamp);
+ }
+ }
+
+ $stmt = $this->db->prepare("INSERT INTO certificates (name, type, functional_perimeter_id, expiration_date) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$intermediateCertFileName, 'intermediate', $perimeterId, $expirationDate]);
+
+
+ $this->logService->log('info', "Périmètre fonctionnel '$perimeterName' créé avec succès et certificat intermédiaire généré.", $userId, $ipAddress);
+ $_SESSION['success'] = $this->langService->__('perimeter_create_success');
+ } else {
+ $_SESSION['error'] = $this->langService->__('perimeter_create_error', ['output' => htmlspecialchars($output)]);
+ $this->logService->log('error', "Échec création périmètre '$perimeterName': $output", $userId, $ipAddress);
+ }
+
+ header('Location: /perimeters');
+ exit();
+ }
+}
diff --git a/app/src/Controllers/UserController.php b/app/src/Controllers/UserController.php
new file mode 100644
index 0000000..4f467d3
--- /dev/null
+++ b/app/src/Controllers/UserController.php
@@ -0,0 +1,212 @@
+db = Database::getInstance();
+ $this->authService = new AuthService($this->db);
+ $this->logService = new LogService(APP_LOG_PATH);
+ $this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
+ }
+
+ /**
+ * Vérifie si l'utilisateur est un administrateur.
+ * Si non, redirige et affiche un message d'erreur.
+ */
+ private function requireAdmin()
+ {
+ if (!$this->authService->isLoggedIn() || $this->authService->getUserRole() !== 'admin') {
+ $_SESSION['error'] = $this->langService->__('permission_denied');
+ header('Location: /dashboard');
+ exit();
+ }
+ }
+
+ /**
+ * Affiche la liste des utilisateurs.
+ */
+ public function index()
+ {
+ $this->requireAdmin();
+
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+
+ $users = $this->db->query("SELECT id, username, role, created_at FROM users ORDER BY username ASC")->fetchAll();
+
+ $successMessage = $_SESSION['success'] ?? null;
+ unset($_SESSION['success']);
+ $errorMessage = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']);
+
+ require_once APP_ROOT_DIR . '/src/Views/users/index.php';
+ }
+
+ /**
+ * Affiche le formulaire de création d'un nouvel utilisateur.
+ */
+ public function showCreateForm()
+ {
+ $this->requireAdmin();
+
+ global $translations;
+ $currentLang = $this->langService->getLanguage();
+ $darkModeClass = DarkMode::getBodyClass();
+
+ $errorMessage = $_SESSION['error'] ?? null;
+ unset($_SESSION['error']);
+
+ require_once APP_ROOT_DIR . '/src/Views/users/create.php';
+ }
+
+ /**
+ * Traite la soumission du formulaire de création d'utilisateur.
+ */
+ public function create()
+ {
+ $this->requireAdmin();
+
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ header('Location: /users/create');
+ exit();
+ }
+
+ $username = trim($_POST['username'] ?? '');
+ $password = $_POST['password'] ?? '';
+ $role = $_POST['role'] ?? 'user';
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $adminUserId = $this->authService->getUserId();
+
+ if (empty($username) || empty($password)) {
+ $_SESSION['error'] = $this->langService->__('user_create_error_empty_fields');
+ header('Location: /users/create');
+ exit();
+ }
+ if (!in_array($role, ['admin', 'user'])) {
+ $_SESSION['error'] = $this->langService->__('user_create_error_invalid_role');
+ header('Location: /users/create');
+ exit();
+ }
+
+ // Vérifier si l'utilisateur existe déjà
+ $stmt = $this->db->prepare("SELECT COUNT(*) FROM users WHERE username = ?");
+ $stmt->execute([$username]);
+ if ($stmt->fetchColumn() > 0) {
+ $_SESSION['error'] = $this->langService->__('user_create_error_exists', ['username' => htmlspecialchars($username)]);
+ header('Location: /users/create');
+ exit();
+ }
+
+ // Hacher le mot de passe
+ $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
+
+ try {
+ $stmt = $this->db->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
+ $stmt->execute([$username, $hashedPassword, $role]);
+ $newUserId = $this->db->lastInsertId();
+
+ $this->logService->log('info', "Nouvel utilisateur '{$username}' ({$role}) créé par l'administrateur.", $adminUserId, $ipAddress);
+ $_SESSION['success'] = $this->langService->__('user_create_success', ['username' => htmlspecialchars($username)]);
+ } catch (\PDOException $e) {
+ error_log("Erreur lors de la création de l'utilisateur: " . $e->getMessage());
+ $_SESSION['error'] = $this->langService->__('user_create_error_db');
+ $this->logService->log('error', "Échec création utilisateur '{$username}': " . $e->getMessage(), $adminUserId, $ipAddress);
+ }
+
+ header('Location: /users');
+ exit();
+ }
+
+ /**
+ * Supprime un utilisateur.
+ */
+ public function delete()
+ {
+ $this->requireAdmin();
+
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ header('Location: /users');
+ exit();
+ }
+
+ $userIdToDelete = $_POST['user_id'] ?? null;
+ $ipAddress = $_SERVER['REMOTE_ADDR'];
+ $adminUserId = $this->authService->getUserId();
+
+ if (empty($userIdToDelete)) {
+ $_SESSION['error'] = $this->langService->__('user_delete_error_id_missing');
+ header('Location: /users');
+ exit();
+ }
+
+ // Empêcher un admin de se supprimer lui-même (ou le dernier admin)
+ if ($userIdToDelete == $adminUserId) {
+ $_SESSION['error'] = $this->langService->__('user_delete_error_self_delete');
+ header('Location: /users');
+ exit();
+ }
+
+ // Vérifier s'il reste au moins un administrateur après la suppression
+ $stmt = $this->db->prepare("SELECT role FROM users WHERE id = ?");
+ $stmt->execute([$userIdToDelete]);
+ $userRoleToDelete = $stmt->fetchColumn();
+
+ if ($userRoleToDelete === 'admin') {
+ $stmt = $this->db->query("SELECT COUNT(*) FROM users WHERE role = 'admin'");
+ $adminCount = $stmt->fetchColumn();
+ if ($adminCount <= 1) {
+ $_SESSION['error'] = $this->langService->__('user_delete_error_last_admin');
+ header('Location: /users');
+ exit();
+ }
+ }
+
+
+ try {
+ // Récupérer le nom d'utilisateur avant suppression pour le log
+ $stmt = $this->db->prepare("SELECT username FROM users WHERE id = ?");
+ $stmt->execute([$userIdToDelete]);
+ $usernameToDelete = $stmt->fetchColumn();
+
+ $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?");
+ $stmt->execute([$userIdToDelete]);
+
+ if ($stmt->rowCount() > 0) {
+ $this->logService->log('info', "Utilisateur '{$usernameToDelete}' (ID: {$userIdToDelete}) supprimé par l'administrateur.", $adminUserId, $ipAddress);
+ $_SESSION['success'] = $this->langService->__('user_delete_success', ['username' => htmlspecialchars($usernameToDelete)]);
+ } else {
+ $_SESSION['error'] = $this->langService->__('user_delete_error_not_found');
+ }
+ } catch (\PDOException $e) {
+ error_log("Erreur lors de la suppression de l'utilisateur: " . $e->getMessage());
+ $_SESSION['error'] = $this->langService->__('user_delete_error_db');
+ $this->logService->log('error', "Échec suppression utilisateur ID: {$userIdToDelete}: " . $e->getMessage(), $adminUserId, $ipAddress);
+ }
+
+ header('Location: /users');
+ exit();
+ }
+}
diff --git a/app/src/Core/Autoloader.php b/app/src/Core/Autoloader.php
new file mode 100644
index 0000000..c03dc29
--- /dev/null
+++ b/app/src/Core/Autoloader.php
@@ -0,0 +1,42 @@
+ PDO::ERRMODE_EXCEPTION, // Rapporte les erreurs SQL sous forme d'exceptions
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Récupère les résultats sous forme de tableaux associatifs
+ PDO::ATTR_EMULATE_PREPARES => false, // Désactive l'émulation des requêtes préparées pour une meilleure sécurité
+ ];
+ try {
+ $this->conn = new PDO($dsn, $user, $password, $options);
+ } catch (PDOException $e) {
+ // Log l'erreur plutôt que de l'afficher directement en production
+ error_log("Erreur de connexion à la base de données: " . $e->getMessage());
+ // Relance l'exception après l'avoir logguée
+ throw new PDOException("Impossible de se connecter à la base de données.", (int)$e->getCode());
+ }
+ }
+
+ /**
+ * Connecte à la base de données ou retourne l'instance existante.
+ *
+ * @param string $host Nom d'hôte de la base de données
+ * @param string $dbName Nom de la base de données
+ * @param string $user Nom d'utilisateur de la base de données
+ * @param string $password Mot de passe de la base de données
+ */
+ public static function connect($host, $dbName, $user, $password)
+ {
+ if (self::$instance === null) {
+ self::$instance = new Database($host, $dbName, $user, $password);
+ }
+ }
+
+ /**
+ * Retourne l'instance PDO de la connexion à la base de données.
+ *
+ * @return PDO L'objet de connexion PDO
+ * @throws \Exception Si la connexion n'a pas été établie au préalable
+ */
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ throw new \Exception("La base de données n'est pas connectée. Appelez Database::connect() d'abord.");
+ }
+ return self::$instance->conn;
+ }
+}
diff --git a/app/src/Core/Router.php b/app/src/Core/Router.php
new file mode 100644
index 0000000..ce8383d
--- /dev/null
+++ b/app/src/Core/Router.php
@@ -0,0 +1,91 @@
+authService = new AuthService(Database::getInstance());
+ }
+
+ /**
+ * Ajoute une nouvelle route au routeur.
+ *
+ * @param string $method Méthode HTTP (GET, POST, etc.)
+ * @param string $path Chemin de l'URL (ex: '/', '/dashboard')
+ * @param string $controllerAction Action du contrôleur (ex: 'HomeController@index')
+ * @param bool $requiresAuth Indique si la route nécessite une authentification (true par défaut)
+ */
+ public function addRoute($method, $path, $controllerAction, $requiresAuth = false)
+ {
+ $this->routes[] = [
+ 'method' => $method,
+ 'path' => $path,
+ 'controllerAction' => $controllerAction,
+ 'requiresAuth' => $requiresAuth
+ ];
+ }
+
+ /**
+ * Dispatche la requête entrante vers le contrôleur et l'action correspondants.
+ * Gère également les redirections pour l'authentification et les erreurs 404.
+ */
+ public function dispatch()
+ {
+ // Récupère le chemin de l'URL demandé (sans les paramètres GET)
+ $requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
+ // Récupère la méthode HTTP de la requête (GET, POST, etc.)
+ $requestMethod = $_SERVER['REQUEST_METHOD'];
+
+ foreach ($this->routes as $route) {
+ // Vérifie si la méthode et le chemin correspondent à une route définie
+ if ($route['method'] === $requestMethod && $route['path'] === $requestUri) {
+ // Si la route nécessite une authentification et que l'utilisateur n'est pas connecté
+ if ($route['requiresAuth'] && !$this->authService->isLoggedIn()) {
+ // Redirige vers la page de connexion
+ header('Location: /login');
+ exit();
+ }
+
+ // Sépare le nom du contrôleur et de l'action
+ list($controllerName, $actionName) = explode('@', $route['controllerAction']);
+ // Construit le nom complet de la classe du contrôleur avec son namespace
+ $controllerClass = "App\\Controllers\\" . $controllerName;
+
+ // Vérifie si la classe du contrôleur existe
+ if (class_exists($controllerClass)) {
+ // Instancie le contrôleur
+ $controller = new $controllerClass();
+ // Vérifie si la méthode de l'action existe dans le contrôleur
+ if (method_exists($controller, $actionName)) {
+ // Appelle la méthode de l'action
+ $controller->$actionName();
+ return; // Termine l'exécution après avoir traité la route
+ }
+ }
+ }
+ }
+
+ // Si aucune route correspondante n'est trouvée, retourne une erreur 404
+ http_response_code(404);
+ // Utilisation de DarkMode::getBodyClass() pour éviter l'erreur de syntaxe
+ echo "404 Non Trouvé";
+ echo "
404 Non Trouvé
";
+ echo "
La page que vous avez demandée n'a pas pu être trouvée.
";
+ }
+}
diff --git a/app/src/Lang/de.json b/app/src/Lang/de.json
new file mode 100644
index 0000000..90c16d4
--- /dev/null
+++ b/app/src/Lang/de.json
@@ -0,0 +1,84 @@
+{
+ "app_name": "Zertifikatsverwaltung",
+ "login_title": "Login - Zertifikatsverwaltung",
+ "login_heading": "Anmeldung zur Anwendung",
+ "username": "Benutzername",
+ "password": "Passwort",
+ "login_button": "Anmelden",
+ "dark_mode": "Dunkler Modus",
+ "light_mode": "Heller Modus",
+ "dashboard_title": "Dashboard",
+ "welcome": "Willkommen, {username}!",
+ "logout": "Abmelden",
+ "certificates": "Zertifikate",
+ "functional_perimeters": "Funktionale Perimeter",
+ "users": "Benutzer",
+ "quick_actions": "Schnellaktionen",
+ "create_new_certificate": "Neues Zertifikat erstellen",
+ "create_new_perimeter": "Neuen Perimeter erstellen",
+ "new_user": "Neuer Benutzer",
+ "certificate_name": "Zertifikatsname",
+ "type": "Typ",
+ "expiration_date": "Ablaufdatum",
+ "status": "Status",
+ "revoked": "Widerrufen",
+ "active": "Aktiv",
+ "actions": "Aktionen",
+ "revoke_certificate": "Widerrufen",
+ "confirm_revoke": "Sind Sie sicher, dass Sie dieses Zertifikat widerrufen möchten? Diese Aktion ist irreversibel und macht das Zertifikat ungültig.",
+ "perimeter_name": "Perimetername",
+ "intermediate_cert_file": "Zwischenzertifikat-Datei",
+ "created_at": "Erstellt am",
+ "create_perimeter_button": "Perimeter erstellen",
+ "create_new_user": "Neuen Benutzer erstellen",
+ "user_role": "Rolle",
+ "admin": "Administrator",
+ "user": "Benutzer",
+ "create_user_button": "Benutzer erstellen",
+ "delete_user": "Löschen",
+ "confirm_delete_user": "Sind Sie sicher, dass Sie diesen Benutzer löschen möchten? Diese Aktion ist irreversibel.",
+ "new_certificate_heading": "Neues Zertifikat erstellen",
+ "subdomain_name": "Subdomain / CN-Name",
+ "select_perimeter": "Funktionalen Perimeter auswählen",
+ "select_perimeter_placeholder": "Wählen Sie einen Perimeter",
+ "create_certificate": "Zertifikat erstellen",
+ "root": "Root",
+ "intermediate": "Zwischen",
+ "simple": "Einfach",
+ "back_to_dashboard": "Zurück zum Dashboard",
+ "back_to_cert_list": "Zurück zur Zertifikatsliste",
+ "back_to_perimeter_list": "Zurück zur Perimeterliste",
+ "back_to_user_list": "Zurück zur Benutzerliste",
+ "no_certificates_yet": "Es wurden noch keine Zertifikate erstellt.",
+ "no_perimeters_yet": "Es wurden noch keine funktionalen Perimeter erstellt.",
+ "no_users_yet": "Es wurden noch keine Benutzer erstellt.",
+ "login_error_empty_fields": "Bitte geben Sie Ihren Benutzernamen und Ihr Passwort ein.",
+ "login_error_credentials": "Falscher Benutzername oder Passwort.",
+ "permission_denied": "Sie haben nicht die notwendigen Berechtigungen, um auf diese Seite zuzugreifen.",
+ "cert_create_error_empty_fields": "Subdomain-Name und funktionaler Perimeter sind erforderlich.",
+ "cert_create_error_perimeter_not_found": "Ausgewählter funktionaler Perimeter nicht gefunden.",
+ "cert_create_success": "Zertifikat erfolgreich erstellt.",
+ "cert_create_error": "Fehler beim Erstellen des Zertifikats: {output}",
+ "cert_revoke_error_id_missing": "Zertifikats-ID für den Widerruf fehlt.",
+ "cert_revoke_error_not_found": "Zertifikat für den Widerruf nicht gefunden.",
+ "cert_revoke_error_ca_revocation": "ROOT- und INTERMEDIATE-Zertifikate können aus PKI-Sicherheitsgründen nicht über die Schnittstelle widerrufen werden.",
+ "cert_revoke_error_already_revoked": "Dieses Zertifikat ist bereits widerrufen.",
+ "cert_revoke_success": "Zertifikat erfolgreich widerrufen.",
+ "cert_revoke_error": "Fehler beim Widerrufen des Zertifikats: {output}",
+ "perimeter_create_error_empty_name": "Der Name des funktionalen Perimeters ist erforderlich.",
+ "perimeter_create_error_exists": "Ein funktionaler Perimeter mit diesem Namen existiert bereits.",
+ "perimeter_create_success": "Funktionaler Perimeter und sein Zwischenzertifikat erfolgreich erstellt.",
+ "perimeter_create_error": "Fehler beim Erstellen des funktionalen Perimeters: {output}",
+ "user_create_error_empty_fields": "Benutzername und Passwort sind erforderlich.",
+ "user_create_error_invalid_role": "Ungültige Benutzerrolle.",
+ "user_create_error_exists": "Ein Benutzer mit dem Benutzernamen '{username}' existiert bereits.",
+ "user_create_success": "Benutzer '{username}' erfolgreich erstellt.",
+ "user_create_error_db": "Fehler beim Erstellen des Benutzers in der Datenbank.",
+ "user_delete_error_id_missing": "Benutzer-ID für das Löschen fehlt.",
+ "user_delete_error_self_delete": "Sie können Ihr eigenes Konto nicht löschen.",
+ "user_delete_error_last_admin": "Das letzte Administratorkonto kann nicht gelöscht werden.",
+ "user_delete_success": "Benutzer '{username}' erfolgreich gelöscht.",
+ "user_delete_error_not_found": "Benutzer zum Löschen nicht gefunden.",
+ "user_delete_error_db": "Fehler beim Löschen des Benutzers aus der Datenbank.",
+ "self_delete_not_allowed": "Sie können sich nicht selbst löschen."
+}
diff --git a/app/src/Lang/en.json b/app/src/Lang/en.json
new file mode 100644
index 0000000..6951f8a
--- /dev/null
+++ b/app/src/Lang/en.json
@@ -0,0 +1,84 @@
+{
+ "app_name": "Certificate Management",
+ "login_title": "Login - Certificate Management",
+ "login_heading": "Log in to the application",
+ "username": "Username",
+ "password": "Password",
+ "login_button": "Log In",
+ "dark_mode": "Dark Mode",
+ "light_mode": "Light Mode",
+ "dashboard_title": "Dashboard",
+ "welcome": "Welcome, {username}!",
+ "logout": "Log Out",
+ "certificates": "Certificates",
+ "functional_perimeters": "Functional Perimeters",
+ "users": "Users",
+ "quick_actions": "Quick Actions",
+ "create_new_certificate": "Create a new certificate",
+ "create_new_perimeter": "Create a new perimeter",
+ "new_user": "New user",
+ "certificate_name": "Certificate Name",
+ "type": "Type",
+ "expiration_date": "Expiration Date",
+ "status": "Status",
+ "revoked": "Revoked",
+ "active": "Active",
+ "actions": "Actions",
+ "revoke_certificate": "Revoke",
+ "confirm_revoke": "Are you sure you want to revoke this certificate? This action is irreversible and will invalidate the certificate.",
+ "perimeter_name": "Perimeter Name",
+ "intermediate_cert_file": "Intermediate Certificate File",
+ "created_at": "Created On",
+ "create_perimeter_button": "Create Perimeter",
+ "create_new_user": "Create a new user",
+ "user_role": "Role",
+ "admin": "Administrator",
+ "user": "User",
+ "create_user_button": "Create User",
+ "delete_user": "Delete",
+ "confirm_delete_user": "Are you sure you want to delete this user? This action is irreversible.",
+ "new_certificate_heading": "Create a New Certificate",
+ "subdomain_name": "Subdomain / CN Name",
+ "select_perimeter": "Select a Functional Perimeter",
+ "select_perimeter_placeholder": "Choose a perimeter",
+ "create_certificate": "Create Certificate",
+ "root": "Root",
+ "intermediate": "Intermediate",
+ "simple": "Simple",
+ "back_to_dashboard": "Back to Dashboard",
+ "back_to_cert_list": "Back to Certificate List",
+ "back_to_perimeter_list": "Back to Perimeter List",
+ "back_to_user_list": "Back to User List",
+ "no_certificates_yet": "No certificates have been created yet.",
+ "no_perimeters_yet": "No functional perimeters have been created yet.",
+ "no_users_yet": "No users have been created yet.",
+ "login_error_empty_fields": "Please enter your username and password.",
+ "login_error_credentials": "Incorrect username or password.",
+ "permission_denied": "You do not have the necessary permissions to access this page.",
+ "cert_create_error_empty_fields": "Subdomain name and functional perimeter are required.",
+ "cert_create_error_perimeter_not_found": "Selected functional perimeter not found.",
+ "cert_create_success": "Certificate created successfully.",
+ "cert_create_error": "Error creating certificate: {output}",
+ "cert_revoke_error_id_missing": "Certificate ID missing for revocation.",
+ "cert_revoke_error_not_found": "Certificate not found for revocation.",
+ "cert_revoke_error_ca_revocation": "ROOT and INTERMEDIATE certificates cannot be revoked via the interface for PKI security reasons.",
+ "cert_revoke_error_already_revoked": "This certificate is already revoked.",
+ "cert_revoke_success": "Certificate revoked successfully.",
+ "cert_revoke_error": "Error revoking certificate: {output}",
+ "perimeter_create_error_empty_name": "Functional perimeter name is required.",
+ "perimeter_create_error_exists": "A functional perimeter with this name already exists.",
+ "perimeter_create_success": "Functional perimeter and its intermediate certificate created successfully.",
+ "perimeter_create_error": "Error creating functional perimeter: {output}",
+ "user_create_error_empty_fields": "Username and password are required.",
+ "user_create_error_invalid_role": "Invalid user role.",
+ "user_create_error_exists": "A user with the username '{username}' already exists.",
+ "user_create_success": "User '{username}' created successfully.",
+ "user_create_error_db": "Error creating user in the database.",
+ "user_delete_error_id_missing": "User ID missing for deletion.",
+ "user_delete_error_self_delete": "You cannot delete your own account.",
+ "user_delete_error_last_admin": "Cannot delete the last administrator account.",
+ "user_delete_success": "User '{username}' deleted successfully.",
+ "user_delete_error_not_found": "User not found for deletion.",
+ "user_delete_error_db": "Error deleting user from the database.",
+ "self_delete_not_allowed": "You cannot delete yourself."
+}
diff --git a/app/src/Lang/es.json b/app/src/Lang/es.json
new file mode 100644
index 0000000..66cb7ab
--- /dev/null
+++ b/app/src/Lang/es.json
@@ -0,0 +1,84 @@
+{
+ "app_name": "Gestión de Certificados",
+ "login_title": "Iniciar Sesión - Gestión de Certificados",
+ "login_heading": "Iniciar sesión en la aplicación",
+ "username": "Nombre de usuario",
+ "password": "Contraseña",
+ "login_button": "Iniciar Sesión",
+ "dark_mode": "Modo Oscuro",
+ "light_mode": "Modo Claro",
+ "dashboard_title": "Panel de Control",
+ "welcome": "¡Bienvenido, {username}!",
+ "logout": "Cerrar Sesión",
+ "certificates": "Certificados",
+ "functional_perimeters": "Perímetros Funcionales",
+ "users": "Usuarios",
+ "quick_actions": "Acciones Rápidas",
+ "create_new_certificate": "Crear nuevo certificado",
+ "create_new_perimeter": "Crear nuevo perímetro",
+ "new_user": "Nuevo usuario",
+ "certificate_name": "Nombre del Certificado",
+ "type": "Tipo",
+ "expiration_date": "Fecha de Caducidad",
+ "status": "Estado",
+ "revoked": "Revocado",
+ "active": "Activo",
+ "actions": "Acciones",
+ "revoke_certificate": "Revocar",
+ "confirm_revoke": "¿Estás seguro de que deseas revocar este certificado? Esta acción es irreversible y lo invalidará.",
+ "perimeter_name": "Nombre del Perímetro",
+ "intermediate_cert_file": "Archivo de Certificado Intermedio",
+ "created_at": "Creado el",
+ "create_perimeter_button": "Crear Perímetro",
+ "create_new_user": "Crear nuevo usuario",
+ "user_role": "Rol",
+ "admin": "Administrador",
+ "user": "Usuario",
+ "create_user_button": "Crear Usuario",
+ "delete_user": "Eliminar",
+ "confirm_delete_user": "¿Estás seguro de que deseas eliminar a este usuario? Esta acción es irreversible.",
+ "new_certificate_heading": "Crear un Nuevo Certificado",
+ "subdomain_name": "Nombre de Subdominio / CN",
+ "select_perimeter": "Seleccionar un Perímetro Funcional",
+ "select_perimeter_placeholder": "Elige un perímetro",
+ "create_certificate": "Crear Certificado",
+ "root": "Raíz",
+ "intermediate": "Intermedio",
+ "simple": "Simple",
+ "back_to_dashboard": "Volver al Panel de Control",
+ "back_to_cert_list": "Volver a la Lista de Certificados",
+ "back_to_perimeter_list": "Volver a la Lista de Perímetros",
+ "back_to_user_list": "Volver a la Lista de Usuarios",
+ "no_certificates_yet": "Todavía no se han creado certificados.",
+ "no_perimeters_yet": "Todavía no se han creado perímetros funcionales.",
+ "no_users_yet": "Todavía no se han creado usuarios.",
+ "login_error_empty_fields": "Por favor, introduce tu nombre de usuario y contraseña.",
+ "login_error_credentials": "Nombre de usuario o contraseña incorrectos.",
+ "permission_denied": "No tienes los permisos necesarios para acceder a esta página.",
+ "cert_create_error_empty_fields": "El nombre de subdominio y el perímetro funcional son obligatorios.",
+ "cert_create_error_perimeter_not_found": "Perímetro funcional seleccionado no encontrado.",
+ "cert_create_success": "Certificado creado correctamente.",
+ "cert_create_error": "Error al crear el certificado: {output}",
+ "cert_revoke_error_id_missing": "ID de certificado faltante para la revocación.",
+ "cert_revoke_error_not_found": "Certificado no encontrado para la revocación.",
+ "cert_revoke_error_ca_revocation": "Los certificados ROOT e INTERMEDIOS no pueden ser revocados a través de la interfaz por razones de seguridad PKI.",
+ "cert_revoke_error_already_revoked": "Este certificado ya ha sido revocado.",
+ "cert_revoke_success": "Certificado revocado correctamente.",
+ "cert_revoke_error": "Error al revocar el certificado: {output}",
+ "perimeter_create_error_empty_name": "El nombre del perímetro funcional es obligatorio.",
+ "perimeter_create_error_exists": "Ya existe un perímetro funcional con este nombre.",
+ "perimeter_create_success": "Perímetro funcional y su certificado intermedio creados correctamente.",
+ "perimeter_create_error": "Error al crear el perímetro funcional: {output}",
+ "user_create_error_empty_fields": "El nombre de usuario y la contraseña son obligatorios.",
+ "user_create_error_invalid_role": "Rol de usuario no válido.",
+ "user_create_error_exists": "Ya existe un usuario con el nombre '{username}'.",
+ "user_create_success": "Usuario '{username}' creado correctamente.",
+ "user_create_error_db": "Error al crear el usuario en la base de datos.",
+ "user_delete_error_id_missing": "ID de usuario faltante para la eliminación.",
+ "user_delete_error_self_delete": "No puedes eliminar tu propia cuenta.",
+ "user_delete_error_last_admin": "No se puede eliminar la última cuenta de administrador.",
+ "user_delete_success": "Usuario '{username}' eliminado correctamente.",
+ "user_delete_error_not_found": "Usuario no encontrado para la eliminación.",
+ "user_delete_error_db": "Error al eliminar el usuario de la base de datos.",
+ "self_delete_not_allowed": "No puedes eliminarte a ti mismo."
+}
diff --git a/app/src/Lang/fr.json b/app/src/Lang/fr.json
new file mode 100644
index 0000000..203b1bb
--- /dev/null
+++ b/app/src/Lang/fr.json
@@ -0,0 +1,84 @@
+{
+ "app_name": "Gestion Certificat",
+ "login_title": "Connexion - Gestion Certificat",
+ "login_heading": "Connexion à l'application",
+ "username": "Nom d'utilisateur",
+ "password": "Mot de passe",
+ "login_button": "Se connecter",
+ "dark_mode": "Mode Sombre",
+ "light_mode": "Mode Clair",
+ "dashboard_title": "Tableau de Bord",
+ "welcome": "Bienvenue, {username} !",
+ "logout": "Déconnexion",
+ "certificates": "Certificats",
+ "functional_perimeters": "Périmètres Fonctionnels",
+ "users": "Utilisateurs",
+ "quick_actions": "Actions Rapides",
+ "create_new_certificate": "Créer un nouveau certificat",
+ "create_new_perimeter": "Créer un nouveau périmètre",
+ "new_user": "Nouvel utilisateur",
+ "certificate_name": "Nom du certificat",
+ "type": "Type",
+ "expiration_date": "Date d'expiration",
+ "status": "Statut",
+ "revoked": "Révoqué",
+ "active": "Actif",
+ "actions": "Actions",
+ "revoke_certificate": "Révoquer",
+ "confirm_revoke": "Êtes-vous sûr de vouloir révoquer ce certificat ? Cette action est irréversible et rendra le certificat invalide.",
+ "perimeter_name": "Nom du périmètre",
+ "intermediate_cert_file": "Fichier Certificat Intermédiaire",
+ "created_at": "Créé le",
+ "create_perimeter_button": "Créer le périmètre",
+ "create_new_user": "Créer un nouvel utilisateur",
+ "user_role": "Rôle",
+ "admin": "Administrateur",
+ "user": "Utilisateur",
+ "create_user_button": "Créer l'utilisateur",
+ "delete_user": "Supprimer",
+ "confirm_delete_user": "Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.",
+ "new_certificate_heading": "Créer un nouveau certificat",
+ "subdomain_name": "Nom du sous-domaine / CN",
+ "select_perimeter": "Sélectionner un périmètre fonctionnel",
+ "select_perimeter_placeholder": "Choisissez un périmètre",
+ "create_certificate": "Créer le certificat",
+ "root": "Root",
+ "intermediate": "Intermédiaire",
+ "simple": "Simple",
+ "back_to_dashboard": "Retour au Tableau de Bord",
+ "back_to_cert_list": "Retour à la liste des certificats",
+ "back_to_perimeter_list": "Retour à la liste des périmètres",
+ "back_to_user_list": "Retour à la liste des utilisateurs",
+ "no_certificates_yet": "Aucun certificat n'a encore été créé.",
+ "no_perimeters_yet": "Aucun périmètre fonctionnel n'a encore été créé.",
+ "no_users_yet": "Aucun utilisateur n'a encore été créé.",
+ "login_error_empty_fields": "Veuillez saisir votre nom d'utilisateur et votre mot de passe.",
+ "login_error_credentials": "Nom d'utilisateur ou mot de passe incorrect.",
+ "permission_denied": "Vous n'avez pas les permissions nécessaires pour accéder à cette page.",
+ "cert_create_error_empty_fields": "Le nom du sous-domaine et le périmètre fonctionnel sont requis.",
+ "cert_create_error_perimeter_not_found": "Le périmètre fonctionnel sélectionné est introuvable.",
+ "cert_create_success": "Certificat créé avec succès.",
+ "cert_create_error": "Erreur lors de la création du certificat: {output}",
+ "cert_revoke_error_id_missing": "ID du certificat manquant pour la révocation.",
+ "cert_revoke_error_not_found": "Certificat introuvable pour la révocation.",
+ "cert_revoke_error_ca_revocation": "Les certificats ROOT et INTERMÉDIAIRES ne peuvent pas être révoqués via l'interface pour des raisons de sécurité PKI.",
+ "cert_revoke_error_already_revoked": "Ce certificat est déjà révoqué.",
+ "cert_revoke_success": "Certificat révoqué avec succès.",
+ "cert_revoke_error": "Erreur lors de la révocation du certificat: {output}",
+ "perimeter_create_error_empty_name": "Le nom du périmètre fonctionnel est requis.",
+ "perimeter_create_error_exists": "Un périmètre fonctionnel avec ce nom existe déjà.",
+ "perimeter_create_success": "Périmètre fonctionnel et son certificat intermédiaire créés avec succès.",
+ "perimeter_create_error": "Erreur lors de la création du périmètre fonctionnel: {output}",
+ "user_create_error_empty_fields": "Le nom d'utilisateur et le mot de passe sont requis.",
+ "user_create_error_invalid_role": "Rôle d'utilisateur invalide.",
+ "user_create_error_exists": "Un utilisateur avec le nom '{username}' existe déjà.",
+ "user_create_success": "Utilisateur '{username}' créé avec succès.",
+ "user_create_error_db": "Erreur lors de la création de l'utilisateur dans la base de données.",
+ "user_delete_error_id_missing": "ID de l'utilisateur manquant pour la suppression.",
+ "user_delete_error_self_delete": "Vous ne pouvez pas supprimer votre propre compte.",
+ "user_delete_error_last_admin": "Impossible de supprimer le dernier compte administrateur.",
+ "user_delete_success": "Utilisateur '{username}' supprimé avec succès.",
+ "user_delete_error_not_found": "Utilisateur introuvable pour la suppression.",
+ "user_delete_error_db": "Erreur lors de la suppression de l'utilisateur dans la base de données.",
+ "self_delete_not_allowed": "Vous ne pouvez pas vous supprimer vous-même."
+}
diff --git a/app/src/Lang/it.json b/app/src/Lang/it.json
new file mode 100644
index 0000000..2145aa9
--- /dev/null
+++ b/app/src/Lang/it.json
@@ -0,0 +1,84 @@
+{
+ "app_name": "Gestione Certificati",
+ "login_title": "Accesso - Gestione Certificati",
+ "login_heading": "Accedi all'applicazione",
+ "username": "Nome utente",
+ "password": "Password",
+ "login_button": "Accedi",
+ "dark_mode": "Modalità Scura",
+ "light_mode": "Modalità Chiara",
+ "dashboard_title": "Dashboard",
+ "welcome": "Benvenuto, {username}!",
+ "logout": "Esci",
+ "certificates": "Certificati",
+ "functional_perimeters": "Perimetri Funzionali",
+ "users": "Utenti",
+ "quick_actions": "Azioni Rapide",
+ "create_new_certificate": "Crea un nuovo certificato",
+ "create_new_perimeter": "Crea un nuovo perimetro",
+ "new_user": "Nuovo utente",
+ "certificate_name": "Nome Certificato",
+ "type": "Tipo",
+ "expiration_date": "Data di Scadenza",
+ "status": "Stato",
+ "revoked": "Revocato",
+ "active": "Attivo",
+ "actions": "Azioni",
+ "revoke_certificate": "Revoca",
+ "confirm_revoke": "Sei sicuro di voler revocare questo certificato? Questa azione è irreversibile e renderà il certificato non valido.",
+ "perimeter_name": "Nome Perimetro",
+ "intermediate_cert_file": "File Certificato Intermedio",
+ "created_at": "Creato il",
+ "create_perimeter_button": "Crea Perimetro",
+ "create_new_user": "Crea un nuovo utente",
+ "user_role": "Ruolo",
+ "admin": "Amministratore",
+ "user": "Utente",
+ "create_user_button": "Crea Utente",
+ "delete_user": "Elimina",
+ "confirm_delete_user": "Sei sicuro di voler eliminare questo utente? Questa azione è irreversibile.",
+ "new_certificate_heading": "Crea un Nuovo Certificato",
+ "subdomain_name": "Nome Sottodominio / CN",
+ "select_perimeter": "Seleziona un Perimetro Funzionale",
+ "select_perimeter_placeholder": "Scegli un perimetro",
+ "create_certificate": "Crea Certificato",
+ "root": "Root",
+ "intermediate": "Intermedio",
+ "simple": "Semplice",
+ "back_to_dashboard": "Torna alla Dashboard",
+ "back_to_cert_list": "Torna all'Elenco Certificati",
+ "back_to_perimeter_list": "Torna all'Elenco Perimetri",
+ "back_to_user_list": "Torna all'Elenco Utenti",
+ "no_certificates_yet": "Nessun certificato è stato ancora creato.",
+ "no_perimeters_yet": "Nessun perimetro funzionale è stato ancora creato.",
+ "no_users_yet": "Nessun utente è stato ancora creato.",
+ "login_error_empty_fields": "Si prega di inserire nome utente e password.",
+ "login_error_credentials": "Nome utente o password errati.",
+ "permission_denied": "Non si dispone delle autorizzazioni necessarie per accedere a questa pagina.",
+ "cert_create_error_empty_fields": "Nome sottodominio e perimetro funzionale sono obbligatori.",
+ "cert_create_error_perimeter_not_found": "Perimetro funzionale selezionato non trovato.",
+ "cert_create_success": "Certificato creato con successo.",
+ "cert_create_error": "Errore durante la creazione del certificato: {output}",
+ "cert_revoke_error_id_missing": "ID certificato mancante per la revoca.",
+ "cert_revoke_error_not_found": "Certificato non trovato per la revoca.",
+ "cert_revoke_error_ca_revocation": "I certificati ROOT e INTERMEDIATE non possono essere revocati tramite l'interfaccia per motivi di sicurezza PKI.",
+ "cert_revoke_error_already_revoked": "Questo certificato è già stato revocato.",
+ "cert_revoke_success": "Certificato revocato con successo.",
+ "cert_revoke_error": "Errore durante la revoca del certificato: {output}",
+ "perimeter_create_error_empty_name": "Il nome del perimetro funzionale è obbligatorio.",
+ "perimeter_create_error_exists": "Esiste già un perimetro funzionale con questo nome.",
+ "perimeter_create_success": "Perimetro funzionale e il suo certificato intermedio creati con successo.",
+ "perimeter_create_error": "Errore durante la creazione del perimetro funzionale: {output}",
+ "user_create_error_empty_fields": "Nome utente e password sono obbligatori.",
+ "user_create_error_invalid_role": "Ruolo utente non valido.",
+ "user_create_error_exists": "Esiste già un utente con il nome '{username}'.",
+ "user_create_success": "Utente '{username}' creato con successo.",
+ "user_create_error_db": "Errore durante la creazione dell'utente nel database.",
+ "user_delete_error_id_missing": "ID utente mancante per l'eliminazione.",
+ "user_delete_error_self_delete": "Non puoi eliminare il tuo account.",
+ "user_delete_error_last_admin": "Impossibile eliminare l'ultimo account amministratore.",
+ "user_delete_success": "Utente '{username}' eliminato con successo.",
+ "user_delete_error_not_found": "Utente non trovato per l'eliminazione.",
+ "user_delete_error_db": "Errore durante l'eliminazione dell'utente dal database.",
+ "self_delete_not_allowed": "Non puoi eliminare te stesso."
+}
diff --git a/app/src/Lang/pt.json b/app/src/Lang/pt.json
new file mode 100644
index 0000000..1ff5920
--- /dev/null
+++ b/app/src/Lang/pt.json
@@ -0,0 +1,84 @@
+{
+ "app_name": "Gestão de Certificados",
+ "login_title": "Login - Gestão de Certificados",
+ "login_heading": "Fazer login na aplicação",
+ "username": "Nome de Utilizador",
+ "password": "Palavra-passe",
+ "login_button": "Entrar",
+ "dark_mode": "Modo Escuro",
+ "light_mode": "Modo Claro",
+ "dashboard_title": "Painel de Controlo",
+ "welcome": "Bem-vindo, {username}!",
+ "logout": "Sair",
+ "certificates": "Certificados",
+ "functional_perimeters": "Perímetros Funcionais",
+ "users": "Utilizadores",
+ "quick_actions": "Ações Rápidas",
+ "create_new_certificate": "Criar novo certificado",
+ "create_new_perimeter": "Criar novo perímetro",
+ "new_user": "Novo utilizador",
+ "certificate_name": "Nome do Certificado",
+ "type": "Tipo",
+ "expiration_date": "Data de Expiração",
+ "status": "Estado",
+ "revoked": "Revogado",
+ "active": "Ativo",
+ "actions": "Ações",
+ "revoke_certificate": "Revogar",
+ "confirm_revoke": "Tem certeza que deseja revogar este certificado? Esta ação é irreversível e invalidará o certificado.",
+ "perimeter_name": "Nome do Perímetro",
+ "intermediate_cert_file": "Ficheiro de Certificado Intermédio",
+ "created_at": "Criado em",
+ "create_perimeter_button": "Criar Perímetro",
+ "create_new_user": "Criar novo utilizador",
+ "user_role": "Função",
+ "admin": "Administrador",
+ "user": "Utilizador",
+ "create_user_button": "Criar Utilizador",
+ "delete_user": "Eliminar",
+ "confirm_delete_user": "Tem certeza que deseja eliminar este utilizador? Esta ação é irreversível.",
+ "new_certificate_heading": "Criar Novo Certificado",
+ "subdomain_name": "Nome do Subdomínio / CN",
+ "select_perimeter": "Selecionar um Perímetro Funcional",
+ "select_perimeter_placeholder": "Escolha um perímetro",
+ "create_certificate": "Criar Certificado",
+ "root": "Raiz",
+ "intermediate": "Intermédio",
+ "simple": "Simples",
+ "back_to_dashboard": "Voltar ao Painel de Controlo",
+ "back_to_cert_list": "Voltar à Lista de Certificados",
+ "back_to_perimeter_list": "Voltar à Lista de Perímetros",
+ "back_to_user_list": "Voltar à Lista de Utilizadores",
+ "no_certificates_yet": "Nenhum certificado foi criado ainda.",
+ "no_perimeters_yet": "Nenhum perímetro funcional foi criado ainda.",
+ "no_users_yet": "Nenhum utilizador foi criado ainda.",
+ "login_error_empty_fields": "Por favor, insira seu nome de utilizador e palavra-passe.",
+ "login_error_credentials": "Nome de utilizador ou palavra-passe incorretos.",
+ "permission_denied": "Não tem as permissões necessárias para aceder a esta página.",
+ "cert_create_error_empty_fields": "O nome do subdomínio e o perímetro funcional são obrigatórios.",
+ "cert_create_error_perimeter_not_found": "Perímetro funcional selecionado não encontrado.",
+ "cert_create_success": "Certificado criado com sucesso.",
+ "cert_create_error": "Erro ao criar certificado: {output}",
+ "cert_revoke_error_id_missing": "ID do certificado em falta para revogação.",
+ "cert_revoke_error_not_found": "Certificado não encontrado para revogação.",
+ "cert_revoke_error_ca_revocation": "Certificados ROOT e INTERMEDIÁRIOS não podem ser revogados através da interface por razões de segurança PKI.",
+ "cert_revoke_error_already_revoked": "Este certificado já está revogado.",
+ "cert_revoke_success": "Certificado revogado com sucesso.",
+ "cert_revoke_error": "Erro ao revogar certificado: {output}",
+ "perimeter_create_error_empty_name": "O nome do perímetro funcional é obrigatório.",
+ "perimeter_create_error_exists": "Já existe um perímetro funcional com este nome.",
+ "perimeter_create_success": "Perímetro funcional e o seu certificado intermédio criados com sucesso.",
+ "perimeter_create_error": "Erro ao criar perímetro funcional: {output}",
+ "user_create_error_empty_fields": "Nome de utilizador e palavra-passe são obrigatórios.",
+ "user_create_error_invalid_role": "Função de utilizador inválida.",
+ "user_create_error_exists": "Já existe um utilizador com o nome '{username}'.",
+ "user_create_success": "Utilizador '{username}' criado com sucesso.",
+ "user_create_error_db": "Erro ao criar utilizador na base de dados.",
+ "user_delete_error_id_missing": "ID de utilizador em falta para eliminação.",
+ "user_delete_error_self_delete": "Não pode eliminar a sua própria conta.",
+ "user_delete_error_last_admin": "Não é possível eliminar a última conta de administrador.",
+ "user_delete_success": "Utilizador '{username}' eliminado com sucesso.",
+ "user_delete_error_not_found": "Utilizador não encontrado para eliminação.",
+ "user_delete_error_db": "Erro ao eliminar utilizador da base de dados.",
+ "self_delete_not_allowed": "Não pode eliminar-se a si mesmo."
+}
diff --git a/app/src/Services/AuthService.php b/app/src/Services/AuthService.php
new file mode 100644
index 0000000..23d4c24
--- /dev/null
+++ b/app/src/Services/AuthService.php
@@ -0,0 +1,117 @@
+db = $db;
+ // Le chemin du fichier de log doit être défini dans config/app.php
+ $this->logService = new LogService(APP_LOG_PATH);
+ }
+
+ /**
+ * Tente de connecter un utilisateur.
+ *
+ * @param string $username Le nom d'utilisateur.
+ * @param string $password Le mot de passe en clair.
+ * @param string $ipAddress L'adresse IP de la requête de connexion.
+ * @return bool Vrai si la connexion est réussie, faux sinon.
+ */
+ public function login($username, $password, $ipAddress)
+ {
+ $stmt = $this->db->prepare("SELECT id, username, password, role FROM users WHERE username = ?");
+ $stmt->execute([$username]);
+ $user = $stmt->fetch();
+
+ if ($user && password_verify($password, $user['password'])) {
+ // Connexion réussie : enregistre les informations de l session
+ $_SESSION['user_id'] = $user['id'];
+ $_SESSION['username'] = $user['username'];
+ $_SESSION['role'] = $user['role'];
+
+ // Log de l'action de connexion réussie
+ $this->logService->log('info', "Connexion réussie pour l'utilisateur '{$username}'.", $user['id'], $ipAddress);
+ return true;
+ } else {
+ // Connexion échouée : log de la tentative
+ $this->logService->log('warning', "Tentative de connexion échouée pour l'utilisateur '{$username}'.", null, $ipAddress);
+ return false;
+ }
+ }
+
+ /**
+ * Déconnecte l'utilisateur actuellement connecté.
+ *
+ * @param string $ipAddress L'adresse IP de la requête de déconnexion.
+ * @return bool Vrai si la déconnexion est effectuée.
+ */
+ public function logout($ipAddress)
+ {
+ $userId = $_SESSION['user_id'] ?? 'inconnu';
+ $username = $_SESSION['username'] ?? 'inconnu';
+
+ // Détruit toutes les données de session
+ session_destroy();
+ $_SESSION = array(); // Vider le tableau $_SESSION
+
+ // Log de l'action de déconnexion
+ $this->logService->log('info', "Déconnexion de l'utilisateur '{$username}'.", $userId, $ipAddress);
+ return true;
+ }
+
+ /**
+ * Vérifie si un utilisateur est actuellement connecté.
+ *
+ * @return bool Vrai si l'utilisateur est connecté, faux sinon.
+ */
+ public function isLoggedIn()
+ {
+ return isset($_SESSION['user_id']);
+ }
+
+ /**
+ * Récupère le rôle de l'utilisateur connecté.
+ *
+ * @return string|null Le rôle de l'utilisateur ('admin' ou 'user'), ou null si non connecté.
+ */
+ public function getUserRole()
+ {
+ return $_SESSION['role'] ?? null;
+ }
+
+ /**
+ * Récupère l'ID de l'utilisateur connecté.
+ *
+ * @return int|null L'ID de l'utilisateur, ou null si non connecté.
+ */
+ public function getUserId()
+ {
+ return $_SESSION['user_id'] ?? null;
+ }
+
+ /**
+ * Récupère le nom d'utilisateur de l'utilisateur connecté.
+ *
+ * @return string|null Le nom d'utilisateur, ou null si non connecté.
+ */
+ public function getUsername()
+ {
+ return $_SESSION['username'] ?? null;
+ }
+}
diff --git a/app/src/Services/LanguageService.php b/app/src/Services/LanguageService.php
new file mode 100644
index 0000000..a5ee8b6
--- /dev/null
+++ b/app/src/Services/LanguageService.php
@@ -0,0 +1,118 @@
+langDir = rtrim($langDir, '/') . '/'; // Assure qu'il y a un slash final
+ // Récupère la langue de la session ou utilise 'en' par default
+ $this->currentLang = $_SESSION['lang'] ?? 'en';
+ // Vérifie si la langue est supportée, sinon revient à 'en'
+ // CORRIGÉ: La condition était incorrecte
+ if (!in_array($this->currentLang, SUPPORTED_LANGUAGES)) {
+ $this->currentLang = 'en';
+ }
+ }
+
+ /**
+ * Définit la langue de l'application.
+ *
+ * @param string $lang Le code de la langue (ex: 'fr', 'en').
+ * @return bool Vrai si la langue a été définie avec succès, faux sinon.
+ */
+ public function setLanguage($lang)
+ {
+ if (in_array($lang, SUPPORTED_LANGUAGES)) {
+ $_SESSION['lang'] = $lang;
+ $this->currentLang = $lang;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Retourne la langue actuellement sélectionnée.
+ *
+ * @return string Le code de la langue.
+ */
+ public function getLanguage()
+ {
+ return $this->currentLang;
+ }
+
+ /**
+ * Charge et retourne toutes les traductions pour la langue actuelle.
+ *
+ * @return array Tableau associatif des traductions.
+ */
+ public function getTranslations()
+ {
+ $filePath = $this->langDir . $this->currentLang . '.json';
+ if (file_exists($filePath)) {
+ $content = file_get_contents($filePath);
+ if ($content === false) {
+ error_log("LanguageService: Impossible de lire le fichier de traduction: " . $filePath);
+ return [];
+ }
+ $translations = json_decode($content, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ error_log("LanguageService: Erreur de décodage JSON pour le fichier: " . $filePath . " - " . json_last_error_msg());
+ return [];
+ }
+ return $translations;
+ }
+ // Fallback à l'anglais si le fichier de la langue actuelle est manquant
+ $englishFilePath = $this->langDir . 'en.json';
+ if (file_exists($englishFilePath)) {
+ $content = file_get_contents($englishFilePath);
+ if ($content === false) {
+ error_log("LanguageService: Impossible de lire le fichier de traduction anglais de secours: " . $englishFilePath);
+ return [];
+ }
+ $translations = json_decode($content, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ error_log("LanguageService: Erreur de décodage JSON pour le fichier anglais de secours: " . $englishFilePath . " - " . json_last_error_msg());
+ return [];
+ }
+ return $translations;
+ }
+ error_log("LanguageService: Aucun fichier de traduction trouvé pour la langue '" . $this->currentLang . "' ou 'en'.");
+ return []; // Retourne un tableau vide si aucune traduction n'est trouvée
+ }
+
+ /**
+ * Traduit une clé donnée.
+ *
+ * @param string $key La clé de traduction.
+ * @param array $replacements Tableau associatif de [placeholder => valeur] pour les remplacements.
+ * @return string La chaîne traduite ou la clé si non trouvée.
+ */
+ public function __($key, $replacements = [])
+ {
+ // Utilise la variable globale $translations qui est chargée dans index.php
+ global $translations;
+
+ $translatedString = $translations[$key] ?? $key;
+
+ // Effectuer les remplacements de placeholders
+ foreach ($replacements as $placeholder => $value) {
+ $translatedString = str_replace("{" . $placeholder . "}", $value, $translatedString);
+ }
+
+ return $translatedString;
+ }
+}
diff --git a/app/src/Services/LanguageService.php.bak b/app/src/Services/LanguageService.php.bak
new file mode 100644
index 0000000..337b4ec
--- /dev/null
+++ b/app/src/Services/LanguageService.php.bak
@@ -0,0 +1,117 @@
+langDir = rtrim($langDir, '/') . '/'; // Assure qu'il y a un slash final
+ // Récupère la langue de la session ou utilise 'en' par défaut
+ $this->currentLang = $_SESSION['lang'] ?? 'en';
+ // Vérifie si la langue est supportée, sinon revient à 'en'
+ if (!in_array($this->currentLang, SUPPORTED_LANGUAGES)) {
+ $this->currentLang = 'en';
+ }
+ }
+
+ /**
+ * Définit la langue de l'application.
+ *
+ * @param string $lang Le code de la langue (ex: 'fr', 'en').
+ * @return bool Vrai si la langue a été définie avec succès, faux sinon.
+ */
+ public function setLanguage($lang)
+ {
+ if (in_array($lang, SUPPORTED_LANGUAGES)) {
+ $_SESSION['lang'] = $lang;
+ $this->currentLang = $lang;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Retourne la langue actuellement sélectionnée.
+ *
+ * @return string Le code de la langue.
+ */
+ public function getLanguage()
+ {
+ return $this->currentLang;
+ }
+
+ /**
+ * Charge et retourne toutes les traductions pour la langue actuelle.
+ *
+ * @return array Tableau associatif des traductions.
+ */
+ public function getTranslations()
+ {
+ $filePath = $this->langDir . $this->currentLang . '.json';
+ if (file_exists($filePath)) {
+ $content = file_get_contents($filePath);
+ if ($content === false) {
+ error_log("LanguageService: Impossible de lire le fichier de traduction: " . $filePath);
+ return [];
+ }
+ $translations = json_decode($content, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ error_log("LanguageService: Erreur de décodage JSON pour le fichier: " . $filePath . " - " . json_last_error_msg());
+ return [];
+ }
+ return $translations;
+ }
+ // Fallback à l'anglais si le fichier de la langue actuelle est manquant
+ $englishFilePath = $this->langDir . 'en.json';
+ if (file_exists($englishFilePath)) {
+ $content = file_get_contents($englishFilePath);
+ if ($content === false) {
+ error_log("LanguageService: Impossible de lire le fichier de traduction anglais de secours: " . $englishFilePath);
+ return [];
+ }
+ $translations = json_decode($content, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ error_log("LanguageService: Erreur de décodage JSON pour le fichier anglais de secours: " . $englishFilePath . " - " . json_last_error_msg());
+ return [];
+ }
+ return $translations;
+ }
+ error_log("LanguageService: Aucun fichier de traduction trouvé pour la langue '" . $this->currentLang . "' ou 'en'.");
+ return []; // Retourne un tableau vide si aucune traduction n'est trouvée
+ }
+
+ /**
+ * Traduit une clé donnée.
+ *
+ * @param string $key La clé de traduction.
+ * @param array $replacements Tableau associatif de [placeholder => valeur] pour les remplacements.
+ * @return string La chaîne traduite ou la clé si non trouvée.
+ */
+ public function __($key, $replacements = [])
+ {
+ // Utilise la variable globale $translations qui est chargée dans index.php
+ global $translations;
+
+ $translatedString = $translations[$key] ?? $key;
+
+ // Effectuer les remplacements de placeholders
+ foreach ($replacements as $placeholder => $value) {
+ $translatedString = str_replace("{" . $placeholder . "}", $value, $translatedString);
+ }
+
+ return $translatedString;
+ }
+}
diff --git a/app/src/Services/LogService.php b/app/src/Services/LogService.php
new file mode 100644
index 0000000..1659633
--- /dev/null
+++ b/app/src/Services/LogService.php
@@ -0,0 +1,48 @@
+logFile = $logFile;
+ }
+
+ /**
+ * Enregistre un message dans le fichier de log.
+ *
+ * @param string $level Niveau de gravité du log (ex: 'info', 'warning', 'error').
+ * @param string $message Le message à enregistrer.
+ * @param int|null $userId L'ID de l'utilisateur si l'action est liée à un utilisateur.
+ * @param string|null $ipAddress L'adresse IP d'où provient l'action.
+ */
+ public function log($level, $message, $userId = null, $ipAddress = null)
+ {
+ $timestamp = date('Y-m-d H:i:s'); // Date et heure actuelle
+ $logEntry = sprintf("[%s] [%s] ", $timestamp, strtoupper($level)); // Format de base du log
+
+ // Ajoute l'ID utilisateur si fourni
+ if ($userId !== null) {
+ $logEntry .= "[User: $userId] ";
+ }
+ // Ajoute l'adresse IP si fournie
+ if ($ipAddress !== null) {
+ $logEntry .= "[IP: $ipAddress] ";
+ }
+ $logEntry .= "$message\n"; // Ajoute le message et un saut de ligne
+
+ // Écrit le log dans le fichier, en ajoutant au contenu existant
+ file_put_contents($this->logFile, $logEntry, FILE_APPEND);
+ }
+}
diff --git a/app/src/Utils/DarkMode.php b/app/src/Utils/DarkMode.php
new file mode 100644
index 0000000..ce3e964
--- /dev/null
+++ b/app/src/Utils/DarkMode.php
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ = htmlspecialchars($translations['login_title']) ?>
+
+
+
+
+