Add files via upload

This commit is contained in:
tips-of-mine
2025-06-14 19:03:54 +02:00
committed by GitHub
parent 2df60f551b
commit b17c666c5a
51 changed files with 4363 additions and 0 deletions

View File

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

366
app/public/css/style.css Normal file
View File

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

57
app/public/dark-mode.css Normal file
View File

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

162
app/public/index.php Normal file
View File

@ -0,0 +1,162 @@
<?php
// Démarrer la session PHP
session_start();
// Inclusion des fichiers fondamentaux
require_once __DIR__ . '/../src/Core/Autoloader.php';
require_once __DIR__ . '/../src/Core/Router.php';
require_once __DIR__ . '/../src/Core/Database.php';
require_once __DIR__ . '/../src/config/app.php'; // Charge les constantes d'application
// Enregistrement de l'autoloader pour charger les classes automatiquement
\App\Core\Autoloader::register();
// Importation des classes à utiliser
use App\Core\Database;
use App\Core\Router;
use App\Services\AuthService;
use App\Services\LanguageService;
use App\Services\LogService;
use App\Utils\DarkMode;
// Initialisation de la connexion à la base de données
try {
Database::connect(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD);
} catch (PDOException $e) {
// En cas d'erreur de connexion, logguer et afficher un message générique
error_log("Database connection error: " . $e->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 "<!DOCTYPE html><html lang=\"fr\"><head><meta charset=\"UTF-8\"><title>Initialisation</title><link rel=\"stylesheet\" href=\"/css/style.css\"></head><body>";
echo "<div class=\"container\"><h1>Initialisation de l'Application</h1>";
echo "<p>Ceci est le premier lancement. Nous allons configurer la base de données, créer le certificat Root CA et le premier compte administrateur.</p>";
// Création du certificat Root CA si non existant
if (!$rootCertExists) {
echo "<p>Création du certificat Root CA en cours...</p>";
$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 "<p>Certificat Root CA créé avec succès.</p>";
// 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 "<p style=\"color: red;\">Erreur lors de la création du certificat Root CA. Veuillez vérifier les logs PHP et Docker.</p>";
echo "<pre>" . htmlspecialchars($output) . "</pre>";
// 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 "<p>Création du premier compte administrateur...</p>";
$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 "<p>Compte administrateur 'admin' créé avec succès. Mot de passe initial: <b>{$adminPasswordPlain}</b> (veuillez le changer après la première connexion !)</p>";
}
echo "<p>Initialisation terminée. Redirection vers la page de connexion dans 5 secondes...</p>";
echo "</div></body></html>";
// 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();

162
app/public/index.php.bak Normal file
View File

@ -0,0 +1,162 @@
<?php
// Démarrer la session PHP
session_start();
// Inclusion des fichiers fondamentaux
require_once __DIR__ . '/../src/Core/Autoloader.php';
require_once __DIR__ . '/../src/Core/Router.php';
require_once __DIR__ . '/../src/Core/Database.php';
require_once __DIR__ . '/../src/config/app.php'; // Charge les constantes d'application
// Enregistrement de l'autoloader pour charger les classes automatiquement
\App\Core\Autoloader::register();
// Importation des classes à utiliser
use App\Core\Database;
use App\Core\Router;
use App\Services\AuthService;
use App\Services\LanguageService;
use App\Services\LogService;
use App\Utils\DarkMode;
// Initialisation de la connexion à la base de données
try {
Database::connect(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD);
} catch (PDOException $e) {
// En cas d'erreur de connexion, logguer et afficher un message générique
error_log("Database connection error: " . $e->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 "<!DOCTYPE html><html lang=\"fr\"><head><meta charset=\"UTF-8\"><title>Initialisation</title><link rel=\"stylesheet\" href=\"/css/style.css\"></head><body>";
echo "<div class=\"container\"><h1>Initialisation de l'Application</h1>";
echo "<p>Ceci est le premier lancement. Nous allons configurer la base de données, créer le certificat Root CA et le premier compte administrateur.</p>";
// Création du certificat Root CA si non existant
if (!$rootCertExists) {
echo "<p>Création du certificat Root CA en cours...</p>";
$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 "<p>Certificat Root CA créé avec succès.</p>";
// 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 "<p style=\"color: red;\">Erreur lors de la création du certificat Root CA. Veuillez vérifier les logs PHP et Docker.</p>";
echo "<pre>" . htmlspecialchars($output) . "</pre>";
// 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 "<p>Création du premier compte administrateur...</p>";
$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 "<p>Compte administrateur 'admin' créé avec succès. Mot de passe initial: <b>{$adminPasswordPlain}</b> (veuillez le changer après la première connexion !)</p>";
}
echo "<p>Initialisation terminée. Redirection vers la page de connexion dans 5 secondes...</p>";
echo "</div></body></html>";
// 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();

View File

@ -0,0 +1,47 @@
<?php
// Démarrer la session PHP (si nécessaire pour la journalisation utilisateur, sinon peut être omis)
session_start();
// Inclusion des fichiers fondamentaux
require_once __DIR__ . '/../src/Core/Autoloader.php';
require_once __DIR__ . '/../src/Core/Database.php';
require_once __DIR__ . '/../src/config/app.php'; // Charge les constantes d'application
// Enregistrement de l'autoloader
\App\Core\Autoloader::register();
// Importation des classes
use App\Core\Database;
use App\Controllers\OcspController; // Votre contrôleur spécifique pour l'OCSP
use App\Services\AuthService;
use App\Services\LogService;
// Initialisation de la connexion à la base de données (si le contrôleur OCSP en a besoin)
try {
Database::connect(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD);
} catch (PDOException $e) {
error_log("OCSP: Database connection error: " . $e->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']);

View File

@ -0,0 +1,47 @@
<?php
// Démarrer la session PHP (si nécessaire pour la journalisation utilisateur, sinon peut être omis)
session_start();
// Inclusion des fichiers fondamentaux
require_once __DIR__ . '/../src/Core/Autoloader.php';
require_once __DIR__ . '/../src/Core/Database.php';
require_once __DIR__ . '/../src/config/app.php'; // Charge les constantes d'application
// Enregistrement de l'autoloader
\App\Core\Autoloader::register();
// Importation des classes
use App\Core\Database;
use App\Controllers\OcspController; // Votre contrôleur spécifique pour l'OCSP
use App\Services\AuthService;
use App\Services\LogService;
// Initialisation de la connexion à la base de données (si le contrôleur OCSP en a besoin)
try {
Database::connect(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD);
} catch (PDOException $e) {
error_log("OCSP: Database connection error: " . $e->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']);

366
app/public/style.css Normal file
View File

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

View File

@ -0,0 +1,98 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
use App\Services\LogService; // Pour les logs de connexion
/**
* Contrôleur pour la gestion de l'authentification (connexion, déconnexion).
*/
class AuthController
{
private $authService;
private $langService;
private $logService;
/**
* Constructeur du AuthController.
*/
public function __construct()
{
$db = Database::getInstance();
$this->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();
}
}

View File

@ -0,0 +1,251 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LogService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
/**
* Contrôleur pour la gestion des certificats.
* (Création, révocation, affichage).
*/
class CertificateController
{
private $db;
private $authService;
private $logService;
private $langService;
/**
* Constructeur du CertificateController.
*/
public function __construct()
{
$this->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();
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
/**
* Contrôleur pour la page du tableau de bord.
*/
class DashboardController
{
private $authService;
private $langService;
/**
* Constructeur du DashboardController.
*/
public function __construct()
{
$this->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';
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
/**
* Contrôleur pour la page d'accueil.
* Redirige vers le tableau de bord si l'utilisateur est déjà connecté.
*/
class HomeController
{
private $authService;
private $langService;
/**
* Constructeur du HomeController.
*/
public function __construct()
{
$this->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();
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\LogService;
/**
* Contrôleur simple pour simuler un répondeur OCSP.
* ATTENTION: Ce n'est pas une implémentation robuste d'un répondeur OCSP de production.
* Un répondeur OCSP réel écouterait les requêtes binaires et répondrait en conséquence.
* Ce contrôleur est juste pour illustrer le point d'entrée.
*/
class OcspController
{
private $db;
private $logService;
public function __construct()
{
$this->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
}
}

View File

@ -0,0 +1,147 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LogService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
/**
* Contrôleur pour la gestion des périmètres fonctionnels.
* (Création, affichage).
*/
class PerimeterController
{
private $db;
private $authService;
private $logService;
private $langService;
/**
* Constructeur du PerimeterController.
*/
public function __construct()
{
$this->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();
}
}

View File

@ -0,0 +1,212 @@
<?php
namespace App\Controllers;
use App\Core\Database;
use App\Services\AuthService;
use App\Services\LogService;
use App\Services\LanguageService;
use App\Utils\DarkMode;
/**
* Contrôleur pour la gestion des utilisateurs.
* (Création, suppression, affichage).
* Nécessite un rôle 'admin'.
*/
class UserController
{
private $db;
private $authService;
private $logService;
private $langService;
/**
* Constructeur du UserController.
*/
public function __construct()
{
$this->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();
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Core;
/**
* Autoloader PSR-4 pour les classes de l'application.
*/
class Autoloader
{
/**
* Enregistre l'autoloader dans la pile de chargement de PHP.
*/
public static function register()
{
spl_autoload_register(function ($class) {
// Préfixe du namespace de l'application
$prefix = 'App\\';
// Répertoire de base où se trouvent les fichiers de l'application (src/)
$baseDir = __DIR__ . '/../'; // Cela pointe vers /app/src/
// Vérifie si la classe utilise le préfixe du namespace
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
// Si la classe n'utilise pas notre préfixe, passe au prochain autoloader enregistré
return;
}
// Récupère le nom de la classe relatif au namespace de base
$relativeClass = substr($class, $len);
// Convertit le nom de la classe relatif en chemin de fichier
// Remplace les séparateurs de namespace par des séparateurs de répertoire et ajoute l'extension .php
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
// Si le fichier existe, l'inclut
if (file_exists($file)) {
require $file;
}
});
}
}

71
app/src/Core/Database.php Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace App\Core;
use PDO;
use PDOException;
/**
* Classe singleton pour gérer la connexion à la base de données MySQL.
*/
class Database
{
private static $instance = null; // Instance unique de la connexion PDO
private $conn; // L'objet de connexion PDO
/**
* Constructeur privé pour empêcher l'instanciation directe (Singleton).
*
* @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
* @throws PDOException Si la connexion échoue
*/
private function __construct($host, $dbName, $user, $password)
{
$dsn = "mysql:host=$host;dbname=$dbName;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => 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;
}
}

91
app/src/Core/Router.php Normal file
View File

@ -0,0 +1,91 @@
<?php
namespace App\Core;
use App\Services\AuthService;
use App\Utils\DarkMode; // Assurez-vous d'importer la classe DarkMode
/**
* Simple routeur pour diriger les requêtes HTTP vers les contrôleurs appropriés.
*/
class Router
{
private $routes = []; // Tableau pour stocker toutes les routes définies
private $authService; // Service d'authentification pour vérifier l'accès aux routes protégées
/**
* Constructeur du routeur.
* Initialise le service d'authentification.
*/
public function __construct()
{
$this->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 "<!DOCTYPE html><html lang=\"fr\"><head><meta charset=\"UTF-8\"><title>404 Non Trouvé</title><link rel=\"stylesheet\" href=\"/css/style.css\"></head><body class=\"" . DarkMode::getBodyClass() . "\">";
echo "<div class=\"container\"><h1>404 Non Trouvé</h1>";
echo "<p>La page que vous avez demandée n'a pas pu être trouvée.</p>";
echo "<p><a href=\"/\">Retour à l'accueil</a></p></div></body></html>";
}
}

84
app/src/Lang/de.json Normal file
View File

@ -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."
}

84
app/src/Lang/en.json Normal file
View File

@ -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."
}

84
app/src/Lang/es.json Normal file
View File

@ -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."
}

84
app/src/Lang/fr.json Normal file
View File

@ -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."
}

84
app/src/Lang/it.json Normal file
View File

@ -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."
}

84
app/src/Lang/pt.json Normal file
View File

@ -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."
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Services;
use PDO;
/**
* Service d'authentification utilisateur.
* Gère les connexions, déconnexions et vérifie l'état de l'authentification.
*/
class AuthService
{
private $db; // Instance PDO pour l'accès à la base de données
private $logService; // Service de journalisation
/**
* Constructeur du service d'authentification.
*
* @param PDO $db L'instance PDO de la base de données.
*/
public function __construct(PDO $db)
{
$this->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;
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Services;
/**
* Service pour gérer la langue de l'application et charger les traductions.
*/
class LanguageService
{
private $langDir; // Répertoire où sont stockés les fichiers de traduction
private $currentLang; // Langue actuellement sélectionnée
/**
* Constructeur du service de langue.
* Initialise la langue actuelle à partir de la session ou une valeur par défaut.
*
* @param string $langDir Chemin absolu du répertoire des fichiers de traduction.
*/
public function __construct($langDir)
{
$this->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;
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Services;
/**
* Service pour gérer la langue de l'application et charger les traductions.
*/
class LanguageService
{
private $langDir; // Répertoire où sont stockés les fichiers de traduction
private $currentLang; // Langue actuellement sélectionnée
/**
* Constructeur du service de langue.
* Initialise la langue actuelle à partir de la session ou une valeur par défaut.
*
* @param string $langDir Chemin absolu du répertoire des fichiers de traduction.
*/
public function __construct($langDir)
{
$this->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;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Services;
/**
* Service pour journaliser les actions de l'application dans un fichier.
*/
class LogService
{
private $logFile; // Chemin complet du fichier de log
/**
* Constructeur du service de journalisation.
*
* @param string $logFile Chemin complet du fichier de log.
*/
public function __construct($logFile)
{
$this->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);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Utils;
/**
* Utilitaire pour gérer le mode sombre/clair de l'application via la session.
*/
class DarkMode
{
/**
* Initialise l'état du mode sombre dans la session si ce n'est pas déjà fait.
* Le mode clair est le mode par défaut.
*/
public static function init()
{
if (!isset($_SESSION['dark_mode'])) {
$_SESSION['dark_mode'] = false; // false = mode clair, true = mode sombre
}
}
/**
* Bascule l'état du mode sombre ou le définit explicitement.
*
* @param string|null $mode 'on' pour activer, 'off' pour désactiver, null pour basculer.
*/
public static function toggle($mode = null)
{
if ($mode === 'on') {
$_SESSION['dark_mode'] = true;
} elseif ($mode === 'off') {
$_SESSION['dark_mode'] = false;
} else {
$_SESSION['dark_mode'] = !($_SESSION['dark_mode'] ?? false); // Bascule l'état actuel
}
}
/**
* Vérifie si le mode sombre est activé.
*
* @return bool Vrai si le mode sombre est activé, faux sinon.
*/
public static function isEnabled()
{
return $_SESSION['dark_mode'] ?? false;
}
/**
* Retourne la classe CSS à appliquer au body HTML en fonction de l'état du mode sombre.
*
* @return string La classe CSS ('dark-mode') ou une chaîne vide.
*/
public static function getBodyClass()
{
return self::isEnabled() ? 'dark-mode' : '';
}
}

View File

@ -0,0 +1,51 @@
<?php
// On assume que $translations, $currentLang, $darkModeClass et $error sont définis par le contrôleur
// global $translations; // Déjà global dans index.php si utilisé ici
// global $currentLang;
// global $darkModeClass;
?>
<!DOCTYPE html>
<html lang="<?= htmlspecialchars($currentLang) ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($translations['login_title']) ?></title>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/dark-mode.css">
</head>
<body class="<?= htmlspecialchars($darkModeClass) ?>">
<div class="container">
<h1><?= htmlspecialchars($translations['login_heading']) ?></h1>
<?php if (isset($error)): ?>
<p class="error-message"><?= htmlspecialchars($error); ?></p>
<?php endif; ?>
<form action="/login" method="post">
<label for="username"><?= htmlspecialchars($translations['username']) ?>:</label><br>
<input type="text" id="username" name="username" required autocomplete="username"><br><br>
<label for="password"><?= htmlspecialchars($translations['password']) ?>:</label><br>
<input type="password" id="password" name="password" required autocomplete="current-password"><br><br>
<button type="submit" class="button primary-button"><?= htmlspecialchars($translations['login_button']) ?></button>
</form>
<div class="options">
<!-- Boutons de changement de langue -->
<div class="language-switcher">
<?php foreach (SUPPORTED_LANGUAGES as $lang): ?>
<a href="?lang=<?= htmlspecialchars($lang) ?>" class="<?= $currentLang === $lang ? 'active' : '' ?>"><?= htmlspecialchars(strtoupper($lang)) ?></a>
<?php endforeach; ?>
</div>
<!-- Bouton de bascule du mode sombre -->
<div class="dark-mode-switcher">
<a href="?dark_mode=<?= \App\Utils\DarkMode::isEnabled() ? 'off' : 'on' ?>" class="button secondary-button">
<?= \App\Utils\DarkMode::isEnabled() ? htmlspecialchars($translations['light_mode']) : htmlspecialchars($translations['dark_mode']) ?>
</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,32 @@
<?php
// Variables attendues du contrôleur: $perimeters, $translations, $currentLang, $darkModeClass, $errorMessage
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= htmlspecialchars($translations['new_certificate_heading']) ?></h1>
<a href="/certificates" class="button secondary-button"><?= htmlspecialchars($translations['back_to_cert_list']) ?></a>
<?php if (isset($errorMessage)): ?>
<p class="error-message"><?= htmlspecialchars($errorMessage); ?></p>
<?php endif; ?>
<form action="/certificates/create" method="post">
<label for="subdomain_name"><?= htmlspecialchars($translations['subdomain_name']) ?>:</label><br>
<input type="text" id="subdomain_name" name="subdomain_name" required placeholder="ex: www, api, mail"><br><br>
<label for="functional_perimeter_id"><?= htmlspecialchars($translations['select_perimeter']) ?>:</label><br>
<select id="functional_perimeter_id" name="functional_perimeter_id" required>
<option value="">-- <?= htmlspecialchars($translations['select_perimeter_placeholder']) ?> --</option>
<?php foreach ($perimeters as $perimeter): ?>
<option value="<?= htmlspecialchars($perimeter['id']) ?>"><?= htmlspecialchars($perimeter['name']) ?></option>
<?php endforeach; ?>
</select><br><br>
<button type="submit" class="button primary-button"><?= htmlspecialchars($translations['create_certificate']) ?></button>
</form>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

View File

@ -0,0 +1,71 @@
<?php
// Variables attendues du contrôleur: $groupedCertificates, $translations, $currentLang, $darkModeClass, $successMessage, $errorMessage, $userRole
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= htmlspecialchars($translations['certificates']) ?></h1>
<div class="actions-bar">
<a href="/dashboard" class="button secondary-button"><?= htmlspecialchars($translations['back_to_dashboard']) ?></a>
<a href="/certificates/create" class="button primary-button"><?= htmlspecialchars($translations['create_new_certificate']) ?></a>
</div>
<?php if (isset($successMessage)): ?>
<p class="success-message"><?= htmlspecialchars($successMessage); ?></p>
<?php endif; ?>
<?php if (isset($errorMessage)): ?>
<p class="error-message"><?= htmlspecialchars($errorMessage); ?></p>
<?php endif; ?>
<?php if (empty($groupedCertificates)): ?>
<p><?= htmlspecialchars($translations['no_certificates_yet']) ?></p>
<?php else: ?>
<?php foreach ($groupedCertificates as $perimeterName => $certsInPerimeter): ?>
<h2 class="perimeter-heading"><?= htmlspecialchars($perimeterName) ?></h2>
<div class="table-responsive">
<table>
<thead>
<tr>
<th><?= htmlspecialchars($translations['certificate_name']) ?></th>
<th><?= htmlspecialchars($translations['type']) ?></th>
<th><?= htmlspecialchars($translations['expiration_date']) ?></th>
<th><?= htmlspecialchars($translations['status']) ?></th>
<th><?= htmlspecialchars($translations['actions']) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($certsInPerimeter as $cert): ?>
<tr class="<?= $cert['is_revoked'] ? 'revoked-cert' : '' ?>">
<td><?= htmlspecialchars($cert['name']) ?></td>
<td><?= htmlspecialchars($translations[$cert['type']] ?? $cert['type']) ?></td>
<td><?= htmlspecialchars((new DateTime($cert['expiration_date']))->format('Y-m-d')) ?></td>
<td>
<?php if ($cert['is_revoked']): ?>
<span class="status-revoked"><?= htmlspecialchars($translations['revoked']) ?></span>
(<?= htmlspecialchars((new DateTime($cert['revoked_at']))->format('Y-m-d')) ?>)
<?php else: ?>
<span class="status-active"><?= htmlspecialchars($translations['active']) ?></span>
<?php endif; ?>
</td>
<td>
<?php
// Seuls les certificats 'simple' et non révoqués peuvent être révoqués via l'interface
if (!$cert['is_revoked'] && $cert['type'] === 'simple'): ?>
<form action="/certificates/revoke" method="post" class="inline-form" onsubmit="return confirm('<?= htmlspecialchars($translations['confirm_revoke']) ?>');">
<input type="hidden" name="certificate_id" value="<?= htmlspecialchars($cert['id']) ?>">
<button type="submit" class="button danger-button"><?= htmlspecialchars($translations['revoke_certificate']) ?></button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

View File

@ -0,0 +1,34 @@
<?php
// Variables attendues du contrôleur: $translations, $currentLang, $darkModeClass, $username, $userRole
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= str_replace('{username}', htmlspecialchars($username), htmlspecialchars($translations['welcome'])) ?></h1>
<nav>
<ul>
<li><a href="/certificates"><?= htmlspecialchars($translations['certificates']) ?></a></li>
<li><a href="/perimeters"><?= htmlspecialchars($translations['functional_perimeters']) ?></a></li>
<?php if ($userRole === 'admin'): ?>
<li><a href="/users"><?= htmlspecialchars($translations['users']) ?></a></li>
<?php endif; ?>
<li><a href="/logout" class="button logout-button"><?= htmlspecialchars($translations['logout']) ?></a></li>
</ul>
</nav>
<section class="quick-actions">
<h2><?= htmlspecialchars($translations['quick_actions']) ?></h2>
<ul>
<li><a href="/certificates/create" class="button create-button"><?= htmlspecialchars($translations['create_new_certificate']) ?></a></li>
<li><a href="/perimeters/create" class="button create-button"><?= htmlspecialchars($translations['create_new_perimeter']) ?></a></li>
<?php if ($userRole === 'admin'): ?>
<li><a href="/users/create" class="button create-button"><?= htmlspecialchars($translations['new_user']) ?></a></li>
<?php endif; ?>
</ul>
</section>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

View File

@ -0,0 +1,24 @@
<?php
// Variables attendues du contrôleur: $translations, $currentLang, $darkModeClass, $errorMessage
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= htmlspecialchars($translations['create_new_perimeter']) ?></h1>
<a href="/perimeters" class="button secondary-button"><?= htmlspecialchars($translations['back_to_perimeter_list']) ?></a>
<?php if (isset($errorMessage)): ?>
<p class="error-message"><?= htmlspecialchars($errorMessage); ?></p>
<?php endif; ?>
<form action="/perimeters/create" method="post">
<label for="name"><?= htmlspecialchars($translations['perimeter_name']) ?>:</label><br>
<input type="text" id="name" name="name" required placeholder="ex: Finance, RH, Marketing"><br><br>
<button type="submit" class="button primary-button"><?= htmlspecialchars($translations['create_perimeter_button']) ?></button>
</form>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

View File

@ -0,0 +1,51 @@
<?php
// Variables attendues du contrôleur: $perimeters, $translations, $currentLang, $darkModeClass, $successMessage, $errorMessage
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= htmlspecialchars($translations['functional_perimeters']) ?></h1>
<div class="actions-bar">
<a href="/dashboard" class="button secondary-button"><?= htmlspecialchars($translations['back_to_dashboard']) ?></a>
<?php if ($userRole === 'admin'): ?>
<a href="/perimeters/create" class="button primary-button"><?= htmlspecialchars($translations['create_new_perimeter']) ?></a>
<?php endif; ?>
</div>
<?php if (isset($successMessage)): ?>
<p class="success-message"><?= htmlspecialchars($successMessage); ?></p>
<?php endif; ?>
<?php if (isset($errorMessage)): ?>
<p class="error-message"><?= htmlspecialchars($errorMessage); ?></p>
<?php endif; ?>
<?php if (empty($perimeters)): ?>
<p><?= htmlspecialchars($translations['no_perimeters_yet']) ?></p>
<?php else: ?>
<div class="table-responsive">
<table>
<thead>
<tr>
<th><?= htmlspecialchars($translations['perimeter_name']) ?></th>
<th><?= htmlspecialchars($translations['intermediate_cert_file']) ?></th>
<th><?= htmlspecialchars($translations['created_at']) ?></th>
<!-- Ajouter des actions ici si nécessaire, comme supprimer un périmètre (avec prudence!) -->
</tr>
</thead>
<tbody>
<?php foreach ($perimeters as $perimeter): ?>
<tr>
<td><?= htmlspecialchars($perimeter['name']) ?></td>
<td><?= htmlspecialchars($perimeter['intermediate_cert_name']) ?></td>
<td><?= htmlspecialchars((new DateTime($perimeter['created_at']))->format('Y-m-d H:i:s')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

View File

@ -0,0 +1,8 @@
</main>
<footer class="app-footer">
<div class="container">
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($translations['app_name'] ?? 'Gestion Certificat') ?>. Tous droits réservés.</p>
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,39 @@
<?php
// Variables attendues: $translations, $currentLang, $darkModeClass
?>
<!DOCTYPE html>
<html lang="<?= htmlspecialchars($currentLang) ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($translations['app_name'] ?? 'Gestion Certificat') ?></title>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/dark-mode.css">
<!-- Font Awesome pour les icônes si besoin -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body class="<?= htmlspecialchars($darkModeClass) ?>">
<header class="app-header">
<div class="header-content container">
<div class="app-title">
<h1><?= htmlspecialchars($translations['app_name'] ?? 'Gestion Certificat') ?></h1>
</div>
<div class="header-controls">
<div class="language-switcher">
<?php foreach (SUPPORTED_LANGUAGES as $lang): ?>
<a href="?lang=<?= htmlspecialchars($lang) ?>" class="lang-button <?= $currentLang === $lang ? 'active' : '' ?>"><?= htmlspecialchars(strtoupper($lang)) ?></a>
<?php endforeach; ?>
</div>
<div class="dark-mode-switcher">
<a href="?dark_mode=<?= \App\Utils\DarkMode::isEnabled() ? 'off' : 'on' ?>" class="dark-mode-button">
<?php if (\App\Utils\DarkMode::isEnabled()): ?>
<i class="fas fa-sun"></i> <?= htmlspecialchars($translations['light_mode']) ?>
<?php else: ?>
<i class="fas fa-moon"></i> <?= htmlspecialchars($translations['dark_mode']) ?>
<?php endif; ?>
</a>
</div>
</div>
</div>
</header>
<main>

View File

@ -0,0 +1,33 @@
<?php
// Variables attendues du contrôleur: $translations, $currentLang, $darkModeClass, $errorMessage
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= htmlspecialchars($translations['create_new_user']) ?></h1>
<a href="/users" class="button secondary-button"><?= htmlspecialchars($translations['back_to_user_list']) ?></a>
<?php if (isset($errorMessage)): ?>
<p class="error-message"><?= htmlspecialchars($errorMessage); ?></p>
<?php endif; ?>
<form action="/users/create" method="post">
<label for="username"><?= htmlspecialchars($translations['username']) ?>:</label><br>
<input type="text" id="username" name="username" required autocomplete="new-username"><br><br>
<label for="password"><?= htmlspecialchars($translations['password']) ?>:</label><br>
<input type="password" id="password" name="password" required autocomplete="new-password"><br><br>
<label for="role"><?= htmlspecialchars($translations['user_role']) ?>:</label><br>
<select id="role" name="role" required>
<option value="user"><?= htmlspecialchars($translations['user']) ?></option>
<option value="admin"><?= htmlspecialchars($translations['admin']) ?></option>
</select><br><br>
<button type="submit" class="button primary-button"><?= htmlspecialchars($translations['create_user_button']) ?></button>
</form>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

View File

@ -0,0 +1,59 @@
<?php
// Variables attendues du contrôleur: $users, $translations, $currentLang, $darkModeClass, $successMessage, $errorMessage, $authService (pour getUserId)
require_once APP_ROOT_DIR . '/src/Views/shared/header.php';
?>
<div class="container">
<h1><?= htmlspecialchars($translations['users']) ?></h1>
<div class="actions-bar">
<a href="/dashboard" class="button secondary-button"><?= htmlspecialchars($translations['back_to_dashboard']) ?></a>
<a href="/users/create" class="button primary-button"><?= htmlspecialchars($translations['create_new_user']) ?></a>
</div>
<?php if (isset($successMessage)): ?>
<p class="success-message"><?= htmlspecialchars($successMessage); ?></p>
<?php endif; ?>
<?php if (isset($errorMessage)): ?>
<p class="error-message"><?= htmlspecialchars($errorMessage); ?></p>
<?php endif; ?>
<?php if (empty($users)): ?>
<p><?= htmlspecialchars($translations['no_users_yet']) ?></p>
<?php else: ?>
<div class="table-responsive">
<table>
<thead>
<tr>
<th><?= htmlspecialchars($translations['username']) ?></th>
<th><?= htmlspecialchars($translations['user_role']) ?></th>
<th><?= htmlspecialchars($translations['created_at']) ?></th>
<th><?= htmlspecialchars($translations['actions']) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?= htmlspecialchars($user['username']) ?></td>
<td><?= htmlspecialchars($translations[$user['role']] ?? $user['role']) ?></td>
<td><?= htmlspecialchars((new DateTime($user['created_at']))->format('Y-m-d H:i:s')) ?></td>
<td>
<?php if ($user['id'] !== $authService->getUserId()): // Impossible de supprimer son propre compte ?>
<form action="/users/delete" method="post" class="inline-form" onsubmit="return confirm('<?= htmlspecialchars($translations['confirm_delete_user']) ?>');">
<input type="hidden" name="user_id" value="<?= htmlspecialchars($user['id']) ?>">
<button type="submit" class="button danger-button"><?= htmlspecialchars($translations['delete_user']) ?></button>
</form>
<?php else: ?>
<em><?= htmlspecialchars($translations['self_delete_not_allowed']) ?></em>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php
require_once APP_ROOT_DIR . '/src/Views/shared/footer.php';
?>

33
app/src/config/app.php Normal file
View File

@ -0,0 +1,33 @@
<?php
// Chemin racine de l'application pour les chemins relatifs
define('APP_ROOT_DIR', __DIR__ . '/../..');
// Configuration de la base de données
// Ces valeurs sont aussi définies dans docker-compose.yml pour le conteneur MySQL
define('DB_HOST', getenv('DB_HOST') ?: 'mysql');
define('DB_NAME', getenv('DB_NAME') ?: 'cert_gestion');
define('DB_USER', getenv('DB_USER') ?: 'user');
define('DB_PASSWORD', getenv('DB_PASSWORD') ?: 'password_secret'); // À CHANGER ABSOLUMENT EN PRODUCTION !
// Configuration générale de l'application
define('APP_NAME', 'Gestion Certificat');
define('APP_ENV', 'development'); // 'production' ou 'development'
// Chemins des dossiers des certificats et scripts OpenSSL dans le conteneur PHP-FPM
define('ROOT_CA_PATH', '/opt/tls/root');
define('INTERMEDIATE_CA_PATH_BASE', '/opt/tls/intermediate'); // Base pour les CA intermédiaires par périmètre
define('SCRIPTS_PATH', '/opt/scripts');
// Chemin du fichier de log principal de l'application PHP
define('APP_LOG_PATH', '/var/log/app/app.log');
// Liste des langues supportées par l'application
define('SUPPORTED_LANGUAGES', ['fr', 'en', 'de', 'it', 'pt', 'es']);
// Clé secrète pour la sécurité des sessions (TRÈS IMPORTANT !)
// Générez une chaîne longue et aléatoire pour la production
define('SESSION_SECRET', 'SuperStrongRandomSessionKeyForProduction_ChangeMe_1234567890ABCDEF');
// URL de base pour le service OCSP (doit correspondre à la configuration Nginx et des certificats)
define('OCSP_URL', 'http://ocsp.cert-gestion.local/'); // À adapter à votre domaine réel

69
docker-compose.yml Normal file
View File

@ -0,0 +1,69 @@
version: '3.8'
services:
nginx:
image: nginx:latest
container_name: cert-gestion-nginx
ports:
- "980:80"
- "9443:443" # Optionnel: Pour HTTPS si vous décidez d'ajouter un certificat à Nginx
volumes:
- ./nginx:/etc/nginx/conf.d:ro # Fichiers de configuration Nginx
- ./app/public:/var/www/html:ro # Contenu statique et point d'entrée de l'application
- ./tls:/opt/tls:rw # Accès en lecture aux certificats pour Nginx si besoin (par ex. pour OCSP)
- ./storage/nginx_logs:/var/log/nginx:rw # Volume pour les logs de Nginx
depends_on:
- php-fpm # Nginx dépend de PHP-FPM pour servir l'application
networks:
- cert-gestion-network
restart: unless-stopped # Nouvelle ligne: Redémarre Nginx si le service s'arrête de manière inattendue
php-fpm:
build:
context: ./php # Construit l'image à partir du dossier ./php
dockerfile: Dockerfile
container_name: cert-gestion-php-fpm
volumes:
- ./app:/var/www/html:rw # Code source de l'application PHP (lecture/écriture pour logs, sessions)
- ./scripts:/opt/scripts:rw # Scripts shell pour la gestion des certificats (lecture seule)
- ./tls:/opt/tls:rw # Dossier pour les certificats et clés (lecture/écriture pour création/révocation)
- ./storage/php_logs:/var/log/app:rw # Volume pour les logs de l'application PHP
environment:
# Variables d'environnement pour PHP (par exemple, pour la connexion DB)
# Il est recommandé d'utiliser un fichier .env avec un outil comme phpdotenv en production
DB_HOST: mysql
DB_NAME: cert_gestion
DB_USER: user
DB_PASSWORD: password_secret # À CHANGER POUR LA PRODUCTION
depends_on:
- mysql # PHP-FPM dépend de MySQL
networks:
- cert-gestion-network
restart: unless-stopped # Nouvelle ligne: Redémarre Nginx si le service s'arrête de manière inattendue
command: >
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

54
mysql/init.sql Normal file
View File

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

49
nginx/default.conf Normal file
View File

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

16
nginx/ocsp.conf Normal file
View File

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

51
php/Dockerfile Normal file
View File

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

31
php/php.ini Normal file
View File

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

54
scripts/create_cert.sh Normal file
View File

@ -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 <subdomain_or_cn_name> <functional_perimeter_name>"
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: <subdomain_or_cn_name>.<functional_perimeter_name>.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"

View File

@ -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 <functional_perimeter_name> [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 <<EOF > "$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"

View File

@ -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 <<EOF > "$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"

25
scripts/ocsp_responder.sh Normal file
View File

@ -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_index_file> -CA <CA_cert> -rsigner <OCSP_signer_cert> -rkey <OCSP_signer_key> \
# -issuer <issuer_cert> -cert <certificate_to_check> -url <OCSP_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

55
scripts/revoke_cert.sh Normal file
View File

@ -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 <cert_base_name> <functional_perimeter_name>"
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