mirror of
https://github.com/tips-of-mine/gestion-certificats2.git
synced 2025-07-02 01:08:43 +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%;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user