Modernisation du projet Gestion Certificat

This commit is contained in:
tips-of-mine
2025-06-16 14:36:10 +02:00
committed by GitHub
parent 145476960b
commit f32805f1c1
32 changed files with 9996 additions and 0 deletions

74
app/public/api.php Normal file
View 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();

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

View 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');
}
}
}

View 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'];
}
}
}

View 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
View 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 [];
}
}