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.

"; + echo "

Retour à l'accueil

"; + } +} 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']) ?> + + + + +
+

+ + +

+ + +
+
+

+ +
+

+ + +
+ +
+ +
+ + + +
+ + +
+ + + +
+
+
+ + diff --git a/app/src/Views/certificates/create.php b/app/src/Views/certificates/create.php new file mode 100644 index 0000000..801e16c --- /dev/null +++ b/app/src/Views/certificates/create.php @@ -0,0 +1,32 @@ + + +
+

+ + + +

+ + +
+
+

+ +
+

+ + +
+
+ + diff --git a/app/src/Views/certificates/index.php b/app/src/Views/certificates/index.php new file mode 100644 index 0000000..d06a411 --- /dev/null +++ b/app/src/Views/certificates/index.php @@ -0,0 +1,71 @@ + + +
+

+
+ + +
+ + +

+ + +

+ + + +

+ + $certsInPerimeter): ?> +

+
+ + + + + + + + + + + + + + + + + + + + + +
format('Y-m-d')) ?> + + + (format('Y-m-d')) ?>) + + + + + +
+ + +
+ +
+
+ + +
+ + diff --git a/app/src/Views/dashboard/index.php b/app/src/Views/dashboard/index.php new file mode 100644 index 0000000..0917bd9 --- /dev/null +++ b/app/src/Views/dashboard/index.php @@ -0,0 +1,34 @@ + + +
+

+ + + +
+

+ +
+
+ + diff --git a/app/src/Views/perimeters/create.php b/app/src/Views/perimeters/create.php new file mode 100644 index 0000000..57f6042 --- /dev/null +++ b/app/src/Views/perimeters/create.php @@ -0,0 +1,24 @@ + + +
+

+ + + +

+ + +
+
+

+ + +
+
+ + diff --git a/app/src/Views/perimeters/index.php b/app/src/Views/perimeters/index.php new file mode 100644 index 0000000..c5cbef0 --- /dev/null +++ b/app/src/Views/perimeters/index.php @@ -0,0 +1,51 @@ + + +
+

+
+ + + + +
+ + +

+ + +

+ + + +

+ +
+ + + + + + + + + + + + + + + + + + +
format('Y-m-d H:i:s')) ?>
+
+ +
+ + diff --git a/app/src/Views/shared/footer.php b/app/src/Views/shared/footer.php new file mode 100644 index 0000000..5d70fe1 --- /dev/null +++ b/app/src/Views/shared/footer.php @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/Views/shared/header.php b/app/src/Views/shared/header.php new file mode 100644 index 0000000..20736fb --- /dev/null +++ b/app/src/Views/shared/header.php @@ -0,0 +1,39 @@ + + + + + + + <?= htmlspecialchars($translations['app_name'] ?? 'Gestion Certificat') ?> + + + + + + +
+
+
+

+
+
+
+ + + +
+ +
+
+
+
diff --git a/app/src/Views/users/create.php b/app/src/Views/users/create.php new file mode 100644 index 0000000..647a195 --- /dev/null +++ b/app/src/Views/users/create.php @@ -0,0 +1,33 @@ + + +
+

+ + + +

+ + +
+
+

+ +
+

+ +
+

+ + +
+
+ + diff --git a/app/src/Views/users/index.php b/app/src/Views/users/index.php new file mode 100644 index 0000000..9396bce --- /dev/null +++ b/app/src/Views/users/index.php @@ -0,0 +1,59 @@ + + +
+

+
+ + +
+ + +

+ + +

+ + + +

+ +
+ + + + + + + + + + + + + + + + + + + +
format('Y-m-d H:i:s')) ?> + getUserId()): // Impossible de supprimer son propre compte ?> +
+ + +
+ + + +
+
+ +
+ + diff --git a/app/src/config/app.php b/app/src/config/app.php new file mode 100644 index 0000000..f762da9 --- /dev/null +++ b/app/src/config/app.php @@ -0,0 +1,33 @@ + + bash -c "chown -R www-data:www-data /var/log/app /var/www/html/storage && chmod -R 775 /var/log/app /var/www/html/storage && chmod +x /opt/scripts/*.sh && php-fpm" + + mysql: + image: mysql:8.0 + container_name: cert-gestion-mysql + environment: + MYSQL_ROOT_PASSWORD: root_password_secret # À CHANGER POUR LA PRODUCTION + MYSQL_DATABASE: cert_gestion + MYSQL_USER: user + MYSQL_PASSWORD: password_secret # À CHANGER POUR LA PRODUCTION + volumes: + - mysql_data:/var/lib/mysql # Volume persistant pour les données de la base de données + - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # Script exécuté au premier démarrage pour créer la DB et les tables + networks: + - cert-gestion-network + restart: unless-stopped # Nouvelle ligne: Redémarre Nginx si le service s'arrête de manière inattendue + +volumes: + mysql_data: # Définition du volume nommé pour MySQL + # Ces volumes sont définis implicitement par les chemins de montage bind mounts: + # ./storage/nginx_logs + # ./storage/php_logs + +networks: + cert-gestion-network: + driver: bridge # Réseau bridge pour la communication interne entre les conteneurs diff --git a/mysql/init.sql b/mysql/init.sql new file mode 100644 index 0000000..aea4cff --- /dev/null +++ b/mysql/init.sql @@ -0,0 +1,54 @@ +-- Script d'initialisation de la base de données MySQL +-- Ce script est exécuté automatiquement par le conteneur MySQL au premier démarrage. + +-- Création de la base de données si elle n'existe pas +CREATE DATABASE IF NOT EXISTS `cert_gestion` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Utilisation de la base de données +USE `cert_gestion`; + +-- Table pour stocker les informations des utilisateurs +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `username` VARCHAR(255) UNIQUE NOT NULL, -- Nom d'utilisateur unique + `password` VARCHAR(255) NOT NULL, -- Mot de passe haché + `role` ENUM('admin', 'user') DEFAULT 'user', -- Rôle de l'utilisateur (admin ou utilisateur simple) + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Date de création du compte +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Table pour stocker les périmètres fonctionnels +CREATE TABLE IF NOT EXISTS `functional_perimeters` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) UNIQUE NOT NULL, -- Nom unique du périmètre fonctionnel + `intermediate_cert_name` VARCHAR(255) UNIQUE NOT NULL, -- Nom du fichier du certificat intermédiaire associé + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Date de création du périmètre +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Table pour stocker les informations sur les certificats +CREATE TABLE IF NOT EXISTS `certificates` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(255) UNIQUE NOT NULL, -- Nom du fichier du certificat (ex: www.example.com.pem) + `type` ENUM('root', 'intermediate', 'simple') NOT NULL, -- Type de certificat + `functional_perimeter_id` INT NULL, -- Clé étrangère vers functional_perimeters (NULL pour le certificat root) + `expiration_date` DATETIME NOT NULL, -- Date d'expiration du certificat + `is_revoked` BOOLEAN DEFAULT FALSE, -- Indique si le certificat est révoqué + `revoked_at` DATETIME NULL, -- Date de révocation si révoqué + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Date de création du certificat + FOREIGN KEY (`functional_perimeter_id`) REFERENCES `functional_perimeters`(`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Table pour historiser les actions des utilisateurs +CREATE TABLE IF NOT EXISTS `action_logs` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `user_id` INT NOT NULL, -- ID de l'utilisateur qui a effectué l'action + `action_type` VARCHAR(50) NOT NULL, -- Type d'action (ex: 'login', 'create_cert', 'revoke_cert', 'create_user') + `description` TEXT NOT NULL, -- Description détaillée de l'action + `ip_address` VARCHAR(45) NOT NULL, -- Adresse IP de l'utilisateur + `action_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Date et heure de l'action + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Note sur l'initialisation du premier administrateur et du certificat root: +-- La création du premier compte administrateur et du certificat root +-- sera gérée par l'application PHP au premier lancement pour des raisons de flexibilité et de sécurité. +-- Le mot de passe de l'administrateur sera haché par l'application PHP. diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..ea7917b --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,49 @@ +server { + listen 80; # Écoute sur le port 80 pour HTTP + server_name localhost; # Peut être votre nom de domaine (ex: cert.example.com) + + root /var/www/html/public; # Dossier racine de l'application PHP accessible via Nginx + + index index.php index.html index.htm; # Fichiers d'index à rechercher + + charset utf-8; # Encodage des caractères + + # Configuration pour les fichiers statiques et le routage de l'application + location / { + try_files $uri $uri/ /index.php?$query_string; # Essaye de servir le fichier, sinon passe à index.php + } + + location ~* \.(css|js|gif|png|jpe?g|svg|ico|pdf|fla|swf|woff|woff2|ttf|eot)$ { + expires 30d; # Mettre en cache pour 30 jours + add_header Cache-Control "public, no-transform"; + try_files $uri =404; # Si le fichier n'est pas trouvé, retourne 404 + } + + # Passe les requêtes PHP à PHP-FPM + location ~ \.php$ { + # Vérifiez que le fichier PHP existe +# try_files $uri =404; + # Passe la requête à PHP-FPM + fastcgi_pass php-fpm:9000; # Nom du service PHP-FPM dans docker-compose et son port + fastcgi_index index.php; # Fichier index pour PHP-FPM + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # Chemin complet du script PHP + include fastcgi_params; # Inclut les paramètres FastCGI par défaut + # Ces paramètres sont essentiels pour une configuration FastCGI robuste + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + + # Bloque l'accès aux fichiers .ht* (pour la sécurité) + location ~ /\.ht { + deny all; + } + + # Empêcher l'accès aux fichiers cachés (.git, .env, etc.) + location ~ /\. { + deny all; + } + + # Fichiers de log d'erreurs et d'accès Nginx + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; +} diff --git a/nginx/ocsp.conf b/nginx/ocsp.conf new file mode 100644 index 0000000..d4e9c43 --- /dev/null +++ b/nginx/ocsp.conf @@ -0,0 +1,16 @@ +server { + listen 80; # Écoute sur le port 80 pour le service OCSP + server_name ocsp.cert-gestion.local; # Nom de domaine pour le service OCSP (à modifier) + + # Nginx agira comme un proxy vers le script PHP qui gérera les requêtes OCSP + location / { + fastcgi_pass php-fpm:9000; # Redirige vers le service PHP-FPM + fastcgi_index ocsp_responder.php; # Point d'entrée pour le répondeur OCSP + fastcgi_param SCRIPT_FILENAME /var/www/html/public/ocsp_responder.php; # Chemin du script PHP + include fastcgi_params; + } + + # Fichiers de log spécifiques au service OCSP + error_log /var/log/nginx/ocsp_error.log; + access_log /var/log/nginx/ocsp_access.log; +} diff --git a/php/Dockerfile b/php/Dockerfile new file mode 100644 index 0000000..6a7714c --- /dev/null +++ b/php/Dockerfile @@ -0,0 +1,51 @@ +FROM php:8.3-fpm + +# Installer les dépendances système nécessaires +RUN apt-get update && apt-get install -y \ + build-essential \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ + locales \ + zip \ + jpegoptim optipng pngquant gifsicle \ + vim \ + unzip \ + git \ + curl \ + libzip-dev \ + libonig-dev \ + mysql-common \ + libldap2-dev \ + libicu-dev \ + openssl \ + ca-certificates + +# libpq-dev \ +# libzip-dev \ +# libicu-devel \ +# unzip \ +# git \ +# openssl \ + # Ajout de ca-certificates pour les opérations SSL/TLS +# ca-certificates \ +# && rm -rf /var/lib/apt/lists/* + +# Installer les extensions PHP nécessaires +# pdo_mysql pour la connexion à MySQL +# opcache pour améliorer les performances de PHP +# zip pour les opérations d'archive +# intl pour l'internationalisation +RUN docker-php-ext-install pdo_mysql opcache zip intl + +# Copier le fichier de configuration PHP personnalisé +COPY php.ini /usr/local/etc/php/conf.d/40-custom.ini + +# Définir le répertoire de travail par défaut +WORKDIR /var/www/html + +# Optionnel: Installer Composer si vous utilisez un framework PHP plus avancé +# COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Exposer le port FPM (le port 9000 est le port par défaut pour PHP-FPM) +EXPOSE 9000 diff --git a/php/php.ini b/php/php.ini new file mode 100644 index 0000000..778ab7a --- /dev/null +++ b/php/php.ini @@ -0,0 +1,31 @@ +; Ces paramètres sont pour configurer PHP. +; Ils sont copiés dans le conteneur PHP-FPM. + +; Augmenter le temps d'exécution maximum pour les scripts (utile pour la génération de certificats) +max_execution_time = 300 + +; Augmenter la limite de mémoire pour les scripts +memory_limit = 256M + +; Taille maximale des fichiers uploadés (si votre application permet des uploads) +upload_max_filesize = 128M +post_max_size = 128M + +; Définir le fuseau horaire de l'application +date.timezone = Europe/Paris + +; Affichage des erreurs: 'Off' en production, 'On' ou 'stderr' en développement +display_errors = Off +display_startup_errors = Off + +; Enregistrement des erreurs dans un fichier de log +log_errors = On +error_log = /var/log/app/php_error.log ; Chemin du fichier de log PHP dans le conteneur + +; Paramètres d'Opcache pour les performances (déjà activé par docker-php-ext-install opcache) +; opcache.enable=1 +; opcache.memory_consumption=128 +; opcache.interned_strings_buffer=8 +; opcache.max_accelerated_files=4000 +; opcache.revalidate_freq=60 +; opcache.fast_shutdown=1 diff --git a/scripts/create_cert.sh b/scripts/create_cert.sh new file mode 100644 index 0000000..0abdbc4 --- /dev/null +++ b/scripts/create_cert.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Ce script crée un certificat simple (d'entité finale) signé par le CA intermédiaire +# du périmètre fonctionnel spécifié. +# Il est appelé par l'application PHP. + +# Arguments : +# $1: Nom du sous-domaine (ex: www, api) ou nom commun +# $2: Nom du périmètre fonctionnel (pour identifier le CA intermédiaire à utiliser) + +SUBDOMAIN_OR_CN_NAME="$1" +FUNCTIONAL_PERIMETER_NAME="$2" + +if [ -z "$SUBDOMAIN_OR_CN_NAME" ] || [ -z "$FUNCTIONAL_PERIMETER_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi + +INTERMEDIATE_CA_DIR="/opt/tls/intermediate/$FUNCTIONAL_PERIMETER_NAME" +INTERMEDIATE_CNF="$INTERMEDIATE_CA_DIR/openssl.cnf" + +# Vérifier si le CA intermédiaire existe +if [ ! -f "$INTERMEDIATE_CA_DIR/certs/intermediate.cert.pem" ]; then + echo "Erreur: Le certificat intermédiaire pour le périmètre '$FUNCTIONAL_PERIMETER_NAME' n'existe pas." + exit 1 +fi + +# Nom de fichier pour le nouveau certificat et sa clé +# Le nom du certificat sera au format: ..cert.pem +CERT_BASE_NAME="${SUBDOMAIN_OR_CN_NAME}.${FUNCTIONAL_PERIMETER_NAME}" +KEY_FILE="$INTERMEDIATE_CA_DIR/private/${CERT_BASE_NAME}.key.pem" +CSR_FILE="$INTERMEDIATE_CA_DIR/csr/${CERT_BASE_NAME}.csr.pem" +CERT_FILE="$INTERMEDIATE_CA_DIR/certs/${CERT_BASE_NAME}.cert.pem" + +echo "Démarrage de la création du certificat '$SUBDOMAIN_OR_CN_NAME' pour le périmètre '$FUNCTIONAL_PERIMETER_NAME'..." + +# Générer la clé privée pour le certificat (2048 bits, sans passphrase) +openssl genrsa -out "$KEY_FILE" 2048 +chmod 400 "$KEY_FILE" # Permissions strictes + +# Générer la CSR (Certificate Signing Request) pour le certificat +# Le Common Name (CN) est important pour les certificats SSL/TLS +openssl req -new -sha256 -key "$KEY_FILE" -out "$CSR_FILE" \ + -subj "/C=FR/ST=Hauts-de-France/L=Roubaix/O=GestionCertif/OU=${FUNCTIONAL_PERIMETER_NAME}/CN=${SUBDOMAIN_OR_CN_NAME}.cert-gestion.local" \ + -reqexts usr_cert -config "$INTERMEDIATE_CNF" # Utilise le CNF de l'intermédiaire et ses extensions usr_cert + +# Signer la CSR avec le CA intermédiaire +openssl ca -batch -config "$INTERMEDIATE_CNF" -extensions usr_cert -days 365 -notext -md sha256 \ + -in "$CSR_FILE" \ + -out "$CERT_FILE" + +chmod 444 "$CERT_FILE" # Permissions en lecture seule + +echo "Certificat '$CERT_BASE_NAME' créé avec succès : $CERT_FILE" diff --git a/scripts/create_intermediate_cert.sh b/scripts/create_intermediate_cert.sh new file mode 100644 index 0000000..abd190e --- /dev/null +++ b/scripts/create_intermediate_cert.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +# Ce script crée un certificat CA intermédiaire signé par le Root CA. +# Il est appelé par l'application PHP lors de la création d'un nouveau "périmètre fonctionnel". + +# Arguments : +# $1: Nom du périmètre fonctionnel (utilisé comme nom du dossier et dans le CN du certificat) +# $2: (Optionnel) Phrase secrète pour la clé privée de l'intermédiaire + +FUNCTIONAL_PERIMETER_NAME="$1" +INTERMEDIATE_KEY_PASSPHRASE="$2" # Optionnel + +if [ -z "$FUNCTIONAL_PERIMETER_NAME" ]; then + echo "Usage: $0 [key_passphrase]" + exit 1 +fi + +ROOT_CA_DIR="/opt/tls/root" +INTERMEDIATE_CA_DIR="/opt/tls/intermediate/$FUNCTIONAL_PERIMETER_NAME" +INTERMEDIATE_KEY="$INTERMEDIATE_CA_DIR/private/intermediate.key.pem" +INTERMEDIATE_CSR="$INTERMEDIATE_CA_DIR/csr/intermediate.csr.pem" +INTERMEDIATE_CERT="$INTERMEDIATE_CA_DIR/certs/intermediate.cert.pem" +INTERMEDIATE_CHAIN="$INTERMEDIATE_CA_DIR/certs/ca-chain.cert.pem" +INTERMEDIATE_CNF="$INTERMEDIATE_CA_DIR/openssl.cnf" + +ROOT_CERT="$ROOT_CA_DIR/certs/ca.cert.pem" +ROOT_KEY="$ROOT_CA_DIR/private/ca.key.pem" +ROOT_CNF="$ROOT_CA_DIR/openssl.cnf" + +echo "Démarrage de la création du certificat Intermédiaire pour '$FUNCTIONAL_PERIMETER_NAME'..." + +# Créer les dossiers nécessaires pour la PKI Intermédiaire +mkdir -p "$INTERMEDIATE_CA_DIR/certs" "$INTERMEDIATE_CA_DIR/crl" "$INTERMEDIATE_CA_DIR/newcerts" "$INTERMEDIATE_CA_DIR/private" "$INTERMEDIATE_CA_DIR/csr" + +# Initialiser les fichiers requis par OpenSSL pour une CA intermédiaire +touch "$INTERMEDIATE_CA_DIR/index.txt" +echo 1000 > "$INTERMEDIATE_CA_DIR/serial" +echo 1000 > "$INTERMEDIATE_CA_DIR/crlnumber" + +# Créer le fichier openssl.cnf pour le CA Intermédiaire +cat < "$INTERMEDIATE_CNF" +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = $INTERMEDIATE_CA_DIR +certs = \$dir/certs +crl_dir = \$dir/crl +database = \$dir/index.txt +new_certs_dir = \$dir/newcerts + +certificate = \$dir/certs/intermediate.cert.pem +serial = \$dir/serial +crlnumber = \$dir/crlnumber +crl = \$dir/crl.pem +private_key = \$dir/private/intermediate.key.pem +RANDFILE = \$dir/private/.rand + +x509_extensions = usr_cert + +name_opt = ca_default +cert_opt = ca_default +default_days = 1825 # Durée de validité par défaut (5 ans) +default_crl_days = 30 +default_md = sha256 +preserve = no + +policy = policy_loose # Politique plus souple pour les CA intermédiaires + +[ policy_strict ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ policy_loose ] +countryName = optional +stateOrProvinceName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ usr_cert ] +# Extensions pour les certificats d'entité finale (signés par cette CA intermédiaire) +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +subjectAltName = @alt_names +extendedKeyUsage = clientAuth,serverAuth +# Ajout de l'URL OCSP +authorityInfoAccess = OCSP;URI:http://ocsp.cert-gestion.local/ # TODO: Remplacez par votre vrai nom de domaine OCSP + +[ v3_intermediate_ca ] +# Extensions pour le certificat CA intermédiaire lui-même +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true, pathlen:0 # pathlen:0 signifie qu'elle ne peut signer que des certificats finaux +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +[ alt_names ] +# Exemple d'alt_names, à adapter si nécessaire +DNS.1 = *.${FUNCTIONAL_PERIMETER_NAME}.local +DNS.2 = ${FUNCTIONAL_PERIMETER_NAME}.local + +EOF + +# Générer la clé privée de l'Intermédiaire CA (avec ou sans passphrase) +if [ -n "$INTERMEDIATE_KEY_PASSPHRASE" ]; then + openssl genrsa -aes256 -passout pass:"$INTERMEDIATE_KEY_PASSPHRASE" -out "$INTERMEDIATE_KEY" 2048 +else + openssl genrsa -out "$INTERMEDIATE_KEY" 2048 +fi +chmod 400 "$INTERMEDIATE_KEY" + +# Générer la CSR (Certificate Signing Request) pour l'Intermédiaire CA +openssl req -new -sha256 \ + -key "$INTERMEDIATE_KEY" $([ -n "$INTERMEDIATE_KEY_PASSPHRASE" ] && echo "-passin pass:\"$INTERMEDIATE_KEY_PASSPHRASE\"") \ + -out "$INTERMEDIATE_CSR" \ + -subj "/C=FR/ST=Hauts-de-France/L=Roubaix/O=GestionCertif/OU=$FUNCTIONAL_PERIMETER_NAME/CN=GestionCertif $FUNCTIONAL_PERIMETER_NAME Intermediate CA" \ + -config "$INTERMEDIATE_CNF" # Utilise le CNF de l'intermédiaire pour la création de la CSR + +# Signer la CSR de l'Intermédiaire avec le Root CA +openssl ca -batch -config "$ROOT_CNF" -extensions v3_intermediate_ca -days 1825 -notext -md sha256 \ + -in "$INTERMEDIATE_CSR" \ + -out "$INTERMEDIATE_CERT" + +chmod 444 "$INTERMEDIATE_CERT" + +# Créer le fichier de chaîne de certificats (Intermediate + Root) +cat "$INTERMEDIATE_CERT" "$ROOT_CERT" > "$INTERMEDIATE_CHAIN" + +echo "Certificat Intermédiaire CA pour '$FUNCTIONAL_PERIMETER_NAME' créé : $INTERMEDIATE_CERT" diff --git a/scripts/create_root_cert.sh b/scripts/create_root_cert.sh new file mode 100644 index 0000000..3631037 --- /dev/null +++ b/scripts/create_root_cert.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Ce script crée le certificat Root CA (Certificate Authority) auto-signé. +# Il est destiné à être exécuté une seule fois, au premier lancement de l'application. + +ROOT_CA_DIR="/opt/tls/root" +ROOT_KEY="$ROOT_CA_DIR/private/ca.key.pem" +ROOT_CERT="$ROOT_CA_DIR/certs/ca.cert.pem" +ROOT_CNF="$ROOT_CA_DIR/openssl.cnf" + +echo "Démarrage de la création du certificat Root CA dans $ROOT_CA_DIR..." + +# Créer les dossiers nécessaires pour la PKI Root +mkdir -p "$ROOT_CA_DIR/certs" "$ROOT_CA_DIR/crl" "$ROOT_CA_DIR/newcerts" "$ROOT_CA_DIR/private" "$ROOT_CA_DIR/csr" + +chmod 777 "$ROOT_CA_DIR/certs" "$ROOT_CA_DIR/crl" "$ROOT_CA_DIR/newcerts" "$ROOT_CA_DIR/private" "$ROOT_CA_DIR/csr" + +# Initialiser les fichiers requis par OpenSSL pour une CA +touch "$ROOT_CA_DIR/index.txt" +echo 1000 > "$ROOT_CA_DIR/serial" # Numéro de série initial pour les certificats +echo 1000 > "$ROOT_CA_DIR/crlnumber" # Numéro de série initial pour la CRL + +# Créer le fichier openssl.cnf pour le Root CA +cat < "$ROOT_CNF" +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = $ROOT_CA_DIR # Répertoire où tout est stocké +certs = \$dir/certs # Répertoire des certificats émis +crl_dir = \$dir/crl # Répertoire des CRL (Listes de révocation de certificats) +database = \$dir/index.txt # Fichier d'index de la base de données (pour OpenSSL) +new_certs_dir = \$dir/newcerts # Emplacement par défaut pour les nouveaux certificats + +certificate = \$dir/certs/ca.cert.pem # Le certificat CA lui-même +serial = \$dir/serial # Le numéro de série actuel +crlnumber = \$dir/crlnumber # Le numéro de CRL actuel +crl = \$dir/crl.pem # Le fichier CRL lui-même +private_key = \$dir/private/ca.key.pem # La clé privée du CA +RANDFILE = \$dir/private/.rand # Fichier de nombres aléatoires privé + +x509_extensions = v3_ca # Extensions X509 à ajouter au certificat +name_opt = ca_default # Options de nom de sujet +cert_opt = ca_default # Options de certificat +default_days = 3650 # Durée de validité par défaut des certificats (10 ans) +default_crl_days = 30 # Durée avant la prochaine mise à jour CRL +default_md = sha256 # Utiliser SHA-256 par défaut +preserve = no # Conserver l'ordre des DN passé + +policy = policy_strict # Politique stricte pour le Root CA + +[ policy_strict ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ v3_ca ] +# Extensions pour le certificat CA Root +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true # C'est une CA, peut signer d'autres certificats +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + +# Ajout de l'URL OCSP au certificat Root CA +# IMPORTANT: Remplacez ocsp.cert-gestion.local par votre vrai nom de domaine OCSP +authorityInfoAccess = OCSP;URI:http://ocsp.cert-gestion.local/ + +EOF + +# Générer la clé privée du Root CA (2048 bits, sans passphrase pour la simplicité) +openssl genrsa -out "$ROOT_KEY" 2048 +chmod 400 "$ROOT_KEY" # Permissions strictes pour la clé privée + +# Générer le certificat Root CA auto-signé +openssl req -x509 -new -nodes -key "$ROOT_KEY" -sha256 -days 3650 -out "$ROOT_CERT" \ + -subj "/C=FR/ST=Hauts-de-France/L=Roubaix/O=GestionCertif/CN=GestionCertif Root CA" \ + -config "$ROOT_CNF" -extensions v3_ca + +chmod 444 "$ROOT_CERT" # Permissions en lecture seule pour le certificat + +echo "Certificat Root CA créé avec succès : $ROOT_CERT" diff --git a/scripts/ocsp_responder.sh b/scripts/ocsp_responder.sh new file mode 100644 index 0000000..318a3e5 --- /dev/null +++ b/scripts/ocsp_responder.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Ce script est un exemple très rudimentaire de ce que ferait un répondeur OCSP. +# En production, un répondeur OCSP serait un service séparé et persistant. +# Ce script ne gère PAS les requêtes OCSP entrantes, il montre juste la commande +# pour générer une réponse OCSP si on lui donne une requête. + +# Arguments attendus si le script était appelé par l'application PHP +# $1: Chemin vers le certificat à vérifier (certificat de l'utilisateur final) +# $2: Chemin vers le certificat du CA émetteur (intermédiaire ou root) +# $3: Chemin vers la clé privée du CA émetteur +# $4: Chemin vers la CRL du CA émetteur + +# Exemple d'utilisation (ce n'est pas ce que Nginx/PHP ferait directement pour chaque requête OCSP) : +# openssl ocsp -index -CA -rsigner -rkey \ +# -issuer -cert -url + +echo "Ce script est un exemple et ne fonctionne pas comme un répondeur OCSP autonome pour HTTP." +echo "Un répondeur OCSP réel devrait écouter les requêtes et y répondre de manière persistante." +echo "Pour simuler une réponse OCSP avec OpenSSL, vous devrez lancer un serveur OCSP comme ceci (en arrière-plan):" +echo "openssl ocsp -index /root/tls/intermediate/mon_perimetre/index.txt -CA /root/tls/intermediate/mon_perimetre/certs/intermediate.cert.pem -rsigner /root/tls/intermediate/mon_perimetre/certs/intermediate.cert.pem -rkey /root/tls/intermediate/mon_perimetre/private/intermediate.key.pem -port 8888" +echo "Puis configurer Nginx pour proxyfier les requêtes à ce port." + +# Pour la démo, si ce script était appelé, il ne ferait rien de fonctionnel en tant que répondeur HTTP. +exit 0 diff --git a/scripts/revoke_cert.sh b/scripts/revoke_cert.sh new file mode 100644 index 0000000..76aae09 --- /dev/null +++ b/scripts/revoke_cert.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Ce script révoque un certificat simple émis par un CA intermédiaire. +# Il met également à jour la CRL (Certificate Revocation List) du CA émetteur. +# Il est appelé par l'application PHP. + +# Arguments : +# $1: Nom du certificat à révoquer (ex: www.finance.cert) +# $2: Nom du périmètre fonctionnel (pour trouver le CA intermédiaire qui l'a émis) + +CERT_BASE_NAME="$1" # Ex: www.finance.cert (sans l'extension .pem) +FUNCTIONAL_PERIMETER_NAME="$2" + +if [ -z "$CERT_BASE_NAME" ] || [ -z "$FUNCTIONAL_PERIMETER_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi + +INTERMEDIATE_CA_DIR="/opt/tls/intermediate/$FUNCTIONAL_PERIMETER_NAME" +INTERMEDIATE_CNF="$INTERMEDIATE_CA_DIR/openssl.cnf" +CERT_PATH="$INTERMEDIATE_CA_DIR/certs/${CERT_BASE_NAME}.cert.pem" +CRL_PATH="$INTERMEDIATE_CA_DIR/crl/crl.pem" + +echo "Démarrage de la révocation du certificat '$CERT_BASE_NAME' pour le périmètre '$FUNCTIONAL_PERIMETER_NAME'..." + +# Vérifier si le certificat existe physiquement et si le CA intermédiaire est prêt +if [ ! -f "$CERT_PATH" ]; then + echo "Erreur: Le certificat '$CERT_PATH' n'existe pas." + exit 1 +fi +if [ ! -f "$INTERMEDIATE_CNF" ]; then + echo "Erreur: Le fichier de configuration OpenSSL pour le CA intermédiaire '$FUNCTIONAL_PERIMETER_NAME' n'existe pas." + exit 1 +fi + +# Révoquer le certificat +# Utilise -batch pour éviter les invites interactives +openssl ca -batch -config "$INTERMEDIATE_CNF" -revoke "$CERT_PATH" + +# Vérifier le succès de la révocation via le code de sortie +if [ $? -ne 0 ]; then + echo "Erreur: La révocation du certificat '$CERT_BASE_NAME' a échoué." + exit 1 +fi + +echo "Certificat '$CERT_BASE_NAME' révoqué avec succès." + +# Mettre à jour la CRL (liste de révocation de certificats) +openssl ca -batch -config "$INTERMEDIATE_CNF" -gencrl -out "$CRL_PATH" + +if [ $? -ne 0 ]; then + echo "Avertissement: La génération de la CRL a échoué. Veuillez vérifier manuellement." +else + echo "CRL mise à jour et disponible à : $CRL_PATH" +fi