mirror of
https://github.com/tips-of-mine/gestion-certificats2.git
synced 2025-06-28 01:18:42 +02:00
Add files via upload
This commit is contained in:
57
app/public/css/dark-mode.css
Normal file
57
app/public/css/dark-mode.css
Normal 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
366
app/public/css/style.css
Normal 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
57
app/public/dark-mode.css
Normal 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
162
app/public/index.php
Normal 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
162
app/public/index.php.bak
Normal 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();
|
47
app/public/ocsp_responder.php
Normal file
47
app/public/ocsp_responder.php
Normal 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']);
|
47
app/public/ocsp_responder.php.bak
Normal file
47
app/public/ocsp_responder.php.bak
Normal 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
366
app/public/style.css
Normal 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%;
|
||||
}
|
||||
}
|
98
app/src/Controllers/AuthController.php
Normal file
98
app/src/Controllers/AuthController.php
Normal 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();
|
||||
}
|
||||
}
|
251
app/src/Controllers/CertificateController.php
Normal file
251
app/src/Controllers/CertificateController.php
Normal 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();
|
||||
}
|
||||
}
|
47
app/src/Controllers/DashboardController.php
Normal file
47
app/src/Controllers/DashboardController.php
Normal 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';
|
||||
}
|
||||
}
|
45
app/src/Controllers/HomeController.php
Normal file
45
app/src/Controllers/HomeController.php
Normal 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();
|
||||
}
|
||||
}
|
76
app/src/Controllers/OcspController.php
Normal file
76
app/src/Controllers/OcspController.php
Normal 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
|
||||
}
|
||||
}
|
147
app/src/Controllers/PerimeterController.php
Normal file
147
app/src/Controllers/PerimeterController.php
Normal 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();
|
||||
}
|
||||
}
|
212
app/src/Controllers/UserController.php
Normal file
212
app/src/Controllers/UserController.php
Normal 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();
|
||||
}
|
||||
}
|
42
app/src/Core/Autoloader.php
Normal file
42
app/src/Core/Autoloader.php
Normal 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
71
app/src/Core/Database.php
Normal 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
91
app/src/Core/Router.php
Normal 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
84
app/src/Lang/de.json
Normal 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
84
app/src/Lang/en.json
Normal 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
84
app/src/Lang/es.json
Normal 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
84
app/src/Lang/fr.json
Normal 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
84
app/src/Lang/it.json
Normal 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
84
app/src/Lang/pt.json
Normal 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."
|
||||
}
|
117
app/src/Services/AuthService.php
Normal file
117
app/src/Services/AuthService.php
Normal 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;
|
||||
}
|
||||
}
|
118
app/src/Services/LanguageService.php
Normal file
118
app/src/Services/LanguageService.php
Normal 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;
|
||||
}
|
||||
}
|
117
app/src/Services/LanguageService.php.bak
Normal file
117
app/src/Services/LanguageService.php.bak
Normal 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;
|
||||
}
|
||||
}
|
48
app/src/Services/LogService.php
Normal file
48
app/src/Services/LogService.php
Normal 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);
|
||||
}
|
||||
}
|
56
app/src/Utils/DarkMode.php
Normal file
56
app/src/Utils/DarkMode.php
Normal 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' : '';
|
||||
}
|
||||
}
|
51
app/src/Views/auth/login.php
Normal file
51
app/src/Views/auth/login.php
Normal 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>
|
32
app/src/Views/certificates/create.php
Normal file
32
app/src/Views/certificates/create.php
Normal 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';
|
||||
?>
|
71
app/src/Views/certificates/index.php
Normal file
71
app/src/Views/certificates/index.php
Normal 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';
|
||||
?>
|
34
app/src/Views/dashboard/index.php
Normal file
34
app/src/Views/dashboard/index.php
Normal 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';
|
||||
?>
|
24
app/src/Views/perimeters/create.php
Normal file
24
app/src/Views/perimeters/create.php
Normal 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';
|
||||
?>
|
51
app/src/Views/perimeters/index.php
Normal file
51
app/src/Views/perimeters/index.php
Normal 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';
|
||||
?>
|
8
app/src/Views/shared/footer.php
Normal file
8
app/src/Views/shared/footer.php
Normal file
@ -0,0 +1,8 @@
|
||||
</main>
|
||||
<footer class="app-footer">
|
||||
<div class="container">
|
||||
<p>© <?= date('Y') ?> <?= htmlspecialchars($translations['app_name'] ?? 'Gestion Certificat') ?>. Tous droits réservés.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
39
app/src/Views/shared/header.php
Normal file
39
app/src/Views/shared/header.php
Normal 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>
|
33
app/src/Views/users/create.php
Normal file
33
app/src/Views/users/create.php
Normal 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';
|
||||
?>
|
59
app/src/Views/users/index.php
Normal file
59
app/src/Views/users/index.php
Normal 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
33
app/src/config/app.php
Normal 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
69
docker-compose.yml
Normal 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
54
mysql/init.sql
Normal 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
49
nginx/default.conf
Normal 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
16
nginx/ocsp.conf
Normal 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
51
php/Dockerfile
Normal 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
31
php/php.ini
Normal 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
54
scripts/create_cert.sh
Normal 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"
|
136
scripts/create_intermediate_cert.sh
Normal file
136
scripts/create_intermediate_cert.sh
Normal 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"
|
84
scripts/create_root_cert.sh
Normal file
84
scripts/create_root_cert.sh
Normal 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
25
scripts/ocsp_responder.sh
Normal 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
55
scripts/revoke_cert.sh
Normal 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
|
Reference in New Issue
Block a user