mirror of
https://github.com/tips-of-mine/gestion-certificats2.git
synced 2025-06-28 09:18:42 +02:00
Modernisation du projet Gestion Certificat
This commit is contained in:
74
app/public/api.php
Normal file
74
app/public/api.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
// Point d'entrée pour les API V1
|
||||
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';
|
||||
|
||||
// Enregistrement de l'autoloader
|
||||
\App\Core\Autoloader::register();
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Api\V1\Router;
|
||||
use App\Services\LogService;
|
||||
|
||||
// Initialisation de la connexion à la base de données
|
||||
try {
|
||||
Database::connect(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD);
|
||||
} catch (PDOException $e) {
|
||||
error_log("API: Database connection error: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Database connection failed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Headers CORS pour les requêtes cross-origin
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
|
||||
|
||||
// Gérer les requêtes OPTIONS (preflight)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Log des requêtes API
|
||||
$logService = new LogService(APP_LOG_PATH);
|
||||
$logService->log('info', 'API Request: ' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'], null, $_SERVER['REMOTE_ADDR']);
|
||||
|
||||
// Configuration du routeur API
|
||||
$router = new Router();
|
||||
|
||||
// Routes d'authentification
|
||||
$router->addRoute('POST', '/auth/login', 'AuthController', 'login');
|
||||
$router->addRoute('POST', '/auth/logout', 'AuthController', 'logout', true);
|
||||
$router->addRoute('GET', '/auth/me', 'AuthController', 'me', true);
|
||||
|
||||
// Routes des certificats
|
||||
$router->addRoute('GET', '/certificates', 'CertificatesController', 'index', true);
|
||||
$router->addRoute('POST', '/certificates', 'CertificatesController', 'create', true);
|
||||
$router->addRoute('POST', '/certificates/{id}/revoke', 'CertificatesController', 'revoke', true);
|
||||
$router->addRoute('GET', '/certificates/download', 'CertificatesController', 'download', true);
|
||||
$router->addRoute('GET', '/certificates/stats', 'CertificatesController', 'stats', true);
|
||||
|
||||
// Routes des périmètres
|
||||
$router->addRoute('GET', '/perimeters', 'PerimetersController', 'index', true);
|
||||
$router->addRoute('POST', '/perimeters', 'PerimetersController', 'create', true);
|
||||
|
||||
// Routes des utilisateurs
|
||||
$router->addRoute('GET', '/users', 'UsersController', 'index', true);
|
||||
$router->addRoute('POST', '/users', 'UsersController', 'create', true);
|
||||
$router->addRoute('DELETE', '/users/{id}', 'UsersController', 'delete', true);
|
||||
$router->addRoute('PUT', '/users/{id}/role', 'UsersController', 'updateRole', true);
|
||||
$router->addRoute('PUT', '/users/{id}/password', 'UsersController', 'updatePassword', true);
|
||||
|
||||
// Route du dashboard
|
||||
$router->addRoute('GET', '/dashboard/stats', 'DashboardController', 'stats', true);
|
||||
|
||||
// Dispatche la requête
|
||||
$router->dispatch();
|
113
app/src/Api/V1/Controllers/AuthController.php
Normal file
113
app/src/Api/V1/Controllers/AuthController.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\V1\Controllers;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Services\AuthService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\LanguageService;
|
||||
use App\Api\V1\Responses\ApiResponse;
|
||||
|
||||
/**
|
||||
* API Controller pour l'authentification.
|
||||
*/
|
||||
class AuthController
|
||||
{
|
||||
private $authService;
|
||||
private $logService;
|
||||
private $langService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$this->authService = new AuthService($db);
|
||||
$this->logService = new LogService(APP_LOG_PATH);
|
||||
$this->langService = new LanguageService(APP_ROOT_DIR . '/src/Lang/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connexion utilisateur
|
||||
* POST /api/v1/auth/login
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
return ApiResponse::methodNotAllowed();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['username']) || !isset($input['password'])) {
|
||||
return ApiResponse::badRequest('Username and password are required');
|
||||
}
|
||||
|
||||
$username = trim($input['username']);
|
||||
$password = $input['password'];
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return ApiResponse::badRequest('Username and password cannot be empty');
|
||||
}
|
||||
|
||||
if ($this->authService->login($username, $password, $ipAddress)) {
|
||||
// Génération d'un token JWT simple ou utilisation de la session
|
||||
$user = [
|
||||
'id' => $this->authService->getUserId(),
|
||||
'username' => $this->authService->getUsername(),
|
||||
'role' => $this->authService->getUserRole(),
|
||||
'token' => $this->generateSimpleToken($this->authService->getUserId())
|
||||
];
|
||||
|
||||
return ApiResponse::success($user, 'Login successful');
|
||||
} else {
|
||||
return ApiResponse::unauthorized('Invalid credentials');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Déconnexion utilisateur
|
||||
* POST /api/v1/auth/logout
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
return ApiResponse::methodNotAllowed();
|
||||
}
|
||||
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$this->authService->logout($ipAddress);
|
||||
|
||||
return ApiResponse::success(null, 'Logout successful');
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les informations de l'utilisateur connecté
|
||||
* GET /api/v1/auth/me
|
||||
*/
|
||||
public function me()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
return ApiResponse::methodNotAllowed();
|
||||
}
|
||||
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return ApiResponse::unauthorized('Not authenticated');
|
||||
}
|
||||
|
||||
$user = [
|
||||
'id' => $this->authService->getUserId(),
|
||||
'username' => $this->authService->getUsername(),
|
||||
'role' => $this->authService->getUserRole(),
|
||||
];
|
||||
|
||||
return ApiResponse::success($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un token simple (à remplacer par JWT en production)
|
||||
*/
|
||||
private function generateSimpleToken($userId)
|
||||
{
|
||||
return base64_encode($userId . ':' . time() . ':' . SESSION_SECRET);
|
||||
}
|
||||
}
|
438
app/src/Api/V1/Controllers/CertificatesController.php
Normal file
438
app/src/Api/V1/Controllers/CertificatesController.php
Normal file
@ -0,0 +1,438 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\V1\Controllers;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Services\AuthService;
|
||||
use App\Services\LogService;
|
||||
use App\Services\LanguageService;
|
||||
use App\Api\V1\Responses\ApiResponse;
|
||||
|
||||
/**
|
||||
* API Controller pour la gestion des certificats.
|
||||
*/
|
||||
class CertificatesController
|
||||
{
|
||||
private $db;
|
||||
private $authService;
|
||||
private $logService;
|
||||
private $langService;
|
||||
|
||||
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/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer tous les certificats avec pagination
|
||||
* GET /api/v1/certificates
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return ApiResponse::unauthorized();
|
||||
}
|
||||
|
||||
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
|
||||
$perPage = isset($_GET['per_page']) ? min(100, max(1, intval($_GET['per_page']))) : 50;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Compter le total
|
||||
$totalStmt = $this->db->query("
|
||||
SELECT COUNT(*) as total
|
||||
FROM certificates c
|
||||
LEFT JOIN functional_perimeters fp ON c.functional_perimeter_id = fp.id
|
||||
");
|
||||
$total = $totalStmt->fetch()['total'];
|
||||
|
||||
// Récupérer les certificats avec pagination
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT
|
||||
c.id, c.name, c.type, c.expiration_date, c.is_revoked, c.revoked_at, c.created_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
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$stmt->execute([$perPage, $offset]);
|
||||
$certificates = $stmt->fetchAll();
|
||||
|
||||
$response = [
|
||||
'data' => $certificates,
|
||||
'current_page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => $total,
|
||||
'last_page' => ceil($total / $perPage)
|
||||
];
|
||||
|
||||
return ApiResponse::success($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un nouveau certificat
|
||||
* POST /api/v1/certificates
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return ApiResponse::unauthorized();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
return ApiResponse::methodNotAllowed();
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['subdomain_name']) || !isset($input['functional_perimeter_id'])) {
|
||||
return ApiResponse::badRequest('Subdomain name and functional perimeter ID are required');
|
||||
}
|
||||
|
||||
$subdomainName = trim($input['subdomain_name']);
|
||||
$functionalPerimeterId = $input['functional_perimeter_id'];
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$userId = $this->authService->getUserId();
|
||||
|
||||
if (empty($subdomainName) || empty($functionalPerimeterId)) {
|
||||
return ApiResponse::badRequest('Subdomain name and functional perimeter are required');
|
||||
}
|
||||
|
||||
// Vérifier que le périmètre existe
|
||||
$stmt = $this->db->prepare("SELECT name FROM functional_perimeters WHERE id = ?");
|
||||
$stmt->execute([$functionalPerimeterId]);
|
||||
$perimeter = $stmt->fetch();
|
||||
|
||||
if (!$perimeter) {
|
||||
return ApiResponse::notFound('Functional perimeter not found');
|
||||
}
|
||||
|
||||
$functionalPerimeterName = $perimeter['name'];
|
||||
|
||||
// Extraire ROOT_DOMAIN du certificat CA racine pour SAN et OCSP
|
||||
$rootCaCertPath = ROOT_CA_PATH . '/certs/ca.cert.pem';
|
||||
if (!file_exists($rootCaCertPath)) {
|
||||
$this->logService->log('error', "Certificat CA racine non trouvé pour extraction ROOT_DOMAIN lors de la création de cert feuille.", $userId, $ipAddress);
|
||||
return ApiResponse::serverError('Root certificate not found');
|
||||
}
|
||||
|
||||
$subjectCommand = "openssl x509 -noout -subject -in " . escapeshellarg($rootCaCertPath);
|
||||
$subjectLine = shell_exec($subjectCommand);
|
||||
$rootDomain = null;
|
||||
if ($subjectLine && preg_match('/CN\s*=\s*ca\.([^\/,\s]+)/', $subjectLine, $matches)) {
|
||||
$rootDomain = $matches[1];
|
||||
}
|
||||
|
||||
if (empty($rootDomain)) {
|
||||
$this->logService->log('error', "Impossible d'extraire ROOT_DOMAIN du cert CA racine (pour SAN).", $userId, $ipAddress);
|
||||
return ApiResponse::serverError('Cannot extract root domain');
|
||||
}
|
||||
|
||||
// Construire la valeur SAN
|
||||
$sanValue = "DNS:" . $subdomainName . "." . $functionalPerimeterName . "." . $rootDomain;
|
||||
$ocspUrl = OCSP_URL;
|
||||
|
||||
// Préparer et exécuter la commande
|
||||
$scriptPath = SCRIPTS_PATH . '/create_cert.sh';
|
||||
$command = "OCSP_URL=" . escapeshellarg($ocspUrl) . " SAN=" . escapeshellarg($sanValue) . " " .
|
||||
escapeshellcmd($scriptPath) . ' ' .
|
||||
escapeshellarg($subdomainName) . ' ' .
|
||||
escapeshellarg($functionalPerimeterName);
|
||||
|
||||
$this->logService->log('info', "Tentative de création du certificat '$subdomainName' pour le périmètre '$functionalPerimeterName'.", $userId, $ipAddress);
|
||||
|
||||
$output = shell_exec($command . ' 2>&1');
|
||||
$certBaseNameForCheck = $subdomainName . '.' . $functionalPerimeterName;
|
||||
|
||||
if (strpos($output, "Certificat '" . $certBaseNameForCheck . "' créé avec succès :") !== false) {
|
||||
// Calculer la date d'expiration
|
||||
$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');
|
||||
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 en base
|
||||
$stmt = $this->db->prepare("INSERT INTO certificates (name, type, functional_perimeter_id, expiration_date) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$certFileName, 'simple', $functionalPerimeterId, $expirationDate]);
|
||||
|
||||
$certificateId = $this->db->lastInsertId();
|
||||
|
||||
// Récupérer le certificat créé
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT c.*, 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]);
|
||||
$certificate = $stmt->fetch();
|
||||
|
||||
$this->logService->log('info', "Certificat '{$certFileName}' créé et enregistré.", $userId, $ipAddress);
|
||||
|
||||
return ApiResponse::created($certificate, 'Certificate created successfully');
|
||||
} else {
|
||||
$this->logService->log('error', "Échec création certificat '$subdomainName': $output", $userId, $ipAddress);
|
||||
return ApiResponse::serverError('Failed to create certificate', ['output' => $output]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Révoquer un certificat
|
||||
* POST /api/v1/certificates/{id}/revoke
|
||||
*/
|
||||
public function revoke($certificateId)
|
||||
{
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return ApiResponse::unauthorized();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
return ApiResponse::methodNotAllowed();
|
||||
}
|
||||
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
$userId = $this->authService->getUserId();
|
||||
|
||||
if (empty($certificateId)) {
|
||||
return ApiResponse::badRequest('Certificate ID is required');
|
||||
}
|
||||
|
||||
// Récupérer les informations du certificat
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT c.name, c.type, c.is_revoked, 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) {
|
||||
return ApiResponse::notFound('Certificate not found');
|
||||
}
|
||||
|
||||
if ($cert['type'] === 'root') {
|
||||
return ApiResponse::forbidden('ROOT certificates cannot be revoked through the interface');
|
||||
}
|
||||
|
||||
if ($cert['is_revoked']) {
|
||||
return ApiResponse::badRequest('Certificate is already revoked');
|
||||
}
|
||||
|
||||
// Logique de révocation selon le type
|
||||
if ($cert['type'] === 'intermediate') {
|
||||
return $this->revokeIntermediateCertificate($cert, $certificateId, $userId, $ipAddress);
|
||||
} else {
|
||||
return $this->revokeSimpleCertificate($cert, $certificateId, $userId, $ipAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques des certificats
|
||||
* GET /api/v1/certificates/stats
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return ApiResponse::unauthorized();
|
||||
}
|
||||
|
||||
// Total certificates
|
||||
$totalStmt = $this->db->query("SELECT COUNT(*) as total FROM certificates");
|
||||
$total = $totalStmt->fetch()['total'];
|
||||
|
||||
// Active certificates
|
||||
$activeStmt = $this->db->query("SELECT COUNT(*) as active FROM certificates WHERE is_revoked = FALSE");
|
||||
$active = $activeStmt->fetch()['active'];
|
||||
|
||||
// Revoked certificates
|
||||
$revokedStmt = $this->db->query("SELECT COUNT(*) as revoked FROM certificates WHERE is_revoked = TRUE");
|
||||
$revoked = $revokedStmt->fetch()['revoked'];
|
||||
|
||||
// Certificates expiring in the next 30 days
|
||||
$expiringSoonStmt = $this->db->prepare("
|
||||
SELECT c.*, fp.name as perimeter_name
|
||||
FROM certificates c
|
||||
LEFT JOIN functional_perimeters fp ON c.functional_perimeter_id = fp.id
|
||||
WHERE c.is_revoked = FALSE
|
||||
AND c.expiration_date <= DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||||
ORDER BY c.expiration_date ASC
|
||||
");
|
||||
$expiringSoonStmt->execute();
|
||||
$expiringSoon = $expiringSoonStmt->fetchAll();
|
||||
|
||||
$stats = [
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
'revoked' => $revoked,
|
||||
'expiring_soon' => $expiringSoon
|
||||
];
|
||||
|
||||
return ApiResponse::success($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Télécharger un certificat
|
||||
* GET /api/v1/certificates/download
|
||||
*/
|
||||
public function download()
|
||||
{
|
||||
if (!$this->authService->isLoggedIn()) {
|
||||
return ApiResponse::unauthorized();
|
||||
}
|
||||
|
||||
$type = $_GET['type'] ?? null;
|
||||
$fileName = $_GET['file'] ?? null;
|
||||
$perimeterName = $_GET['perimeter'] ?? null;
|
||||
|
||||
$userId = $this->authService->getUserId();
|
||||
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
$this->logService->log('info', "Download attempt: type='{$type}', file='{$fileName}', perimeter='{$perimeterName}'", $userId, $ipAddress);
|
||||
|
||||
if (empty($type) || empty($fileName)) {
|
||||
return ApiResponse::badRequest('Missing download parameters');
|
||||
}
|
||||
|
||||
if (basename($fileName) !== $fileName || ($perimeterName && basename($perimeterName) !== $perimeterName)) {
|
||||
return ApiResponse::badRequest('Invalid characters in file or perimeter name');
|
||||
}
|
||||
|
||||
$filePath = $this->buildFilePath($type, $fileName, $perimeterName);
|
||||
|
||||
if (!$filePath) {
|
||||
return ApiResponse::badRequest('Invalid certificate type or file');
|
||||
}
|
||||
|
||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||
return ApiResponse::notFound('File not found or not readable');
|
||||
}
|
||||
|
||||
// Check permissions for private keys
|
||||
if (str_ends_with($fileName, '.key.pem') && $this->authService->getUserRole() !== 'admin') {
|
||||
return ApiResponse::forbidden('Unauthorized to download private keys');
|
||||
}
|
||||
|
||||
$this->logService->log('info', "File '{$filePath}' download initiated", $userId, $ipAddress);
|
||||
|
||||
// Set headers for file download
|
||||
$mimeType = 'application/x-pem-file';
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: must-revalidate');
|
||||
header('Pragma: public');
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
|
||||
if (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
readfile($filePath);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function buildFilePath($type, $fileName, $perimeterName)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'root':
|
||||
if ($fileName === 'ca.cert.pem') {
|
||||
return ROOT_CA_PATH . '/certs/' . $fileName;
|
||||
} elseif ($fileName === 'ca.key.pem') {
|
||||
return ROOT_CA_PATH . '/private/' . $fileName;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'intermediate':
|
||||
case 'simple':
|
||||
if (empty($perimeterName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_ends_with($fileName, '.key.pem')) {
|
||||
return INTERMEDIATE_CA_PATH_BASE . '/' . $perimeterName . '/private/' . $fileName;
|
||||
} else {
|
||||
return INTERMEDIATE_CA_PATH_BASE . '/' . $perimeterName . '/certs/' . $fileName;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function revokeIntermediateCertificate($cert, $certificateId, $userId, $ipAddress)
|
||||
{
|
||||
$functionalPerimeterName = $cert['perimeter_name'];
|
||||
$intermediateCertPath = "/opt/tls/intermediate/" . $functionalPerimeterName . "/certs/" . $cert['name'];
|
||||
$rootCaConfigPath = "/opt/tls/root/openssl.cnf";
|
||||
$rootCaCrlPath = "/opt/tls/root/crl/crl.pem";
|
||||
|
||||
$revokeCmd = sprintf(
|
||||
"openssl ca -batch -config %s -revoke %s",
|
||||
escapeshellarg($rootCaConfigPath),
|
||||
escapeshellarg($intermediateCertPath)
|
||||
);
|
||||
|
||||
$this->logService->log('info', "Tentative de révocation du certificat intermédiaire '{$cert['name']}'", $userId, $ipAddress);
|
||||
$outputRevoke = shell_exec($revokeCmd . ' 2>&1');
|
||||
|
||||
if (strpos($outputRevoke, "Data Base Updated") !== false || strpos($outputRevoke, "Successfully revoked certificate") !== false) {
|
||||
$generateCrlCmd = sprintf(
|
||||
"openssl ca -batch -config %s -gencrl -out %s",
|
||||
escapeshellarg($rootCaConfigPath),
|
||||
escapeshellarg($rootCaCrlPath)
|
||||
);
|
||||
|
||||
$outputCrl = shell_exec($generateCrlCmd . ' 2>&1');
|
||||
|
||||
if ((strpos($outputCrl, "CRL Generated") !== false || strpos($outputCrl, "CRL generated") !== false) && file_exists($rootCaCrlPath)) {
|
||||
$stmt_update = $this->db->prepare("UPDATE certificates SET is_revoked = TRUE, revoked_at = NOW() WHERE id = ?");
|
||||
$stmt_update->execute([$certificateId]);
|
||||
|
||||
$this->logService->log('info', "Certificat intermédiaire '{$cert['name']}' révoqué et CRL mise à jour", $userId, $ipAddress);
|
||||
return ApiResponse::success(null, 'Intermediate certificate revoked successfully');
|
||||
} else {
|
||||
$this->logService->log('error', "Échec de la mise à jour de la CRL: $outputCrl", $userId, $ipAddress);
|
||||
return ApiResponse::serverError('Failed to update CRL');
|
||||
}
|
||||
} else {
|
||||
$this->logService->log('error', "Échec révocation certificat intermédiaire: $outputRevoke", $userId, $ipAddress);
|
||||
return ApiResponse::serverError('Failed to revoke intermediate certificate');
|
||||
}
|
||||
}
|
||||
|
||||
private function revokeSimpleCertificate($cert, $certificateId, $userId, $ipAddress)
|
||||
{
|
||||
$certBaseName = str_replace('.cert.pem', '.cert', $cert['name']);
|
||||
$functionalPerimeterName = $cert['perimeter_name'];
|
||||
|
||||
$command = escapeshellcmd(SCRIPTS_PATH . '/revoke_cert.sh') . ' ' .
|
||||
escapeshellarg($certBaseName) . ' ' .
|
||||
escapeshellarg($functionalPerimeterName);
|
||||
|
||||
$this->logService->log('info', "Tentative de révocation du certificat simple '{$cert['name']}'", $userId, $ipAddress);
|
||||
$output = shell_exec($command . ' 2>&1');
|
||||
|
||||
if (strpos($output, "Certificat '$certBaseName' révoqué avec succès.") !== false) {
|
||||
$stmt_update = $this->db->prepare("UPDATE certificates SET is_revoked = TRUE, revoked_at = NOW() WHERE id = ?");
|
||||
$stmt_update->execute([$certificateId]);
|
||||
|
||||
$this->logService->log('info', "Certificat simple '{$cert['name']}' révoqué", $userId, $ipAddress);
|
||||
return ApiResponse::success(null, 'Certificate revoked successfully');
|
||||
} else {
|
||||
$this->logService->log('error', "Échec révocation certificat simple: $output", $userId, $ipAddress);
|
||||
return ApiResponse::serverError('Failed to revoke certificate');
|
||||
}
|
||||
}
|
||||
}
|
92
app/src/Api/V1/Middleware/AuthMiddleware.php
Normal file
92
app/src/Api/V1/Middleware/AuthMiddleware.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\V1\Middleware;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Services\AuthService;
|
||||
use App\Api\V1\Responses\ApiResponse;
|
||||
|
||||
/**
|
||||
* Middleware d'authentification pour les routes API.
|
||||
*/
|
||||
class AuthMiddleware
|
||||
{
|
||||
private $authService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->authService = new AuthService(Database::getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie l'authentification pour les routes protégées.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// Vérifier si l'utilisateur est connecté via session
|
||||
if ($this->authService->isLoggedIn()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier le token Bearer (pour les appels API)
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
if (preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
|
||||
$token = $matches[1];
|
||||
if ($this->validateToken($token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ApiResponse::unauthorized('Authentication required');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation simple du token (à remplacer par JWT en production).
|
||||
*/
|
||||
private function validateToken($token)
|
||||
{
|
||||
try {
|
||||
$decoded = base64_decode($token);
|
||||
$parts = explode(':', $decoded);
|
||||
|
||||
if (count($parts) === 3) {
|
||||
$userId = $parts[0];
|
||||
$timestamp = $parts[1];
|
||||
$signature = $parts[2];
|
||||
|
||||
// Vérifier la signature
|
||||
$expectedSignature = base64_encode($userId . ':' . $timestamp . ':' . SESSION_SECRET);
|
||||
if ($signature === SESSION_SECRET) {
|
||||
// Vérifier que le token n'est pas trop ancien (24h)
|
||||
if ((time() - $timestamp) < 86400) {
|
||||
// Reconstituer la session utilisateur
|
||||
$this->restoreUserSession($userId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Token invalide
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaure la session utilisateur à partir de l'ID.
|
||||
*/
|
||||
private function restoreUserSession($userId)
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT id, username, role FROM users WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['role'] = $user['role'];
|
||||
}
|
||||
}
|
||||
}
|
115
app/src/Api/V1/Responses/ApiResponse.php
Normal file
115
app/src/Api/V1/Responses/ApiResponse.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\V1\Responses;
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour les réponses API standardisées.
|
||||
*/
|
||||
class ApiResponse
|
||||
{
|
||||
/**
|
||||
* Envoie une réponse de succès.
|
||||
*/
|
||||
public static function success($data = null, $message = 'Success', $statusCode = 200)
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une réponse de création réussie.
|
||||
*/
|
||||
public static function created($data = null, $message = 'Created successfully')
|
||||
{
|
||||
return self::success($data, $message, 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une réponse d'erreur.
|
||||
*/
|
||||
public static function error($message = 'An error occurred', $statusCode = 400, $errors = null)
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($errors !== null) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 400 - Bad Request.
|
||||
*/
|
||||
public static function badRequest($message = 'Bad Request', $errors = null)
|
||||
{
|
||||
return self::error($message, 400, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 401 - Unauthorized.
|
||||
*/
|
||||
public static function unauthorized($message = 'Unauthorized')
|
||||
{
|
||||
return self::error($message, 401);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 403 - Forbidden.
|
||||
*/
|
||||
public static function forbidden($message = 'Forbidden')
|
||||
{
|
||||
return self::error($message, 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 404 - Not Found.
|
||||
*/
|
||||
public static function notFound($message = 'Not Found')
|
||||
{
|
||||
return self::error($message, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 405 - Method Not Allowed.
|
||||
*/
|
||||
public static function methodNotAllowed($message = 'Method Not Allowed')
|
||||
{
|
||||
return self::error($message, 405);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 422 - Unprocessable Entity.
|
||||
*/
|
||||
public static function unprocessableEntity($message = 'Validation failed', $errors = null)
|
||||
{
|
||||
return self::error($message, 422, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse d'erreur 500 - Internal Server Error.
|
||||
*/
|
||||
public static function serverError($message = 'Internal Server Error', $errors = null)
|
||||
{
|
||||
return self::error($message, 500, $errors);
|
||||
}
|
||||
}
|
113
app/src/Api/V1/Router.php
Normal file
113
app/src/Api/V1/Router.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\V1;
|
||||
|
||||
use App\Api\V1\Middleware\AuthMiddleware;
|
||||
use App\Api\V1\Responses\ApiResponse;
|
||||
|
||||
/**
|
||||
* Routeur pour les API V1.
|
||||
*/
|
||||
class Router
|
||||
{
|
||||
private $routes = [];
|
||||
private $authMiddleware;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->authMiddleware = new AuthMiddleware();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une route.
|
||||
*/
|
||||
public function addRoute($method, $path, $controller, $action, $requiresAuth = false)
|
||||
{
|
||||
$this->routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'controller' => $controller,
|
||||
'action' => $action,
|
||||
'requiresAuth' => $requiresAuth
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatche la requête vers le bon contrôleur.
|
||||
*/
|
||||
public function dispatch()
|
||||
{
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'];
|
||||
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
|
||||
// Retirer le préfixe /api/v1
|
||||
$requestUri = preg_replace('#^/api/v1#', '', $requestUri);
|
||||
if (empty($requestUri)) {
|
||||
$requestUri = '/';
|
||||
}
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($this->matchRoute($route, $requestMethod, $requestUri)) {
|
||||
// Vérifier l'authentification si nécessaire
|
||||
if ($route['requiresAuth'] && !$this->authMiddleware->handle()) {
|
||||
return; // Le middleware a déjà envoyé la réponse
|
||||
}
|
||||
|
||||
// Extraire les paramètres de l'URL
|
||||
$params = $this->extractParams($route['path'], $requestUri);
|
||||
|
||||
// Instancier le contrôleur et appeler l'action
|
||||
$controllerClass = "App\\Api\\V1\\Controllers\\" . $route['controller'];
|
||||
|
||||
if (class_exists($controllerClass)) {
|
||||
$controller = new $controllerClass();
|
||||
$action = $route['action'];
|
||||
|
||||
if (method_exists($controller, $action)) {
|
||||
// Appeler l'action avec les paramètres
|
||||
call_user_func_array([$controller, $action], $params);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ApiResponse::serverError('Controller or action not found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ApiResponse::notFound('API endpoint not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une route correspond à la requête.
|
||||
*/
|
||||
private function matchRoute($route, $method, $uri)
|
||||
{
|
||||
if ($route['method'] !== $method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convertir le pattern de route en regex
|
||||
$pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $route['path']);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
return preg_match($pattern, $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait les paramètres de l'URL.
|
||||
*/
|
||||
private function extractParams($routePath, $uri)
|
||||
{
|
||||
$pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $routePath);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
if (preg_match($pattern, $uri, $matches)) {
|
||||
// Retirer le premier élément (correspondance complète)
|
||||
array_shift($matches);
|
||||
return $matches;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user