Ajout du dossier api avec la géolocalisation automatique des casernes de pompiers
This commit is contained in:
39
api/src/Core/Database.php
Normal file
39
api/src/Core/Database.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class Database {
|
||||
private static ?PDO $instance = null;
|
||||
private static array $config;
|
||||
|
||||
public static function init(array $config): void {
|
||||
self::$config = $config;
|
||||
}
|
||||
|
||||
public static function getInstance(): PDO {
|
||||
if (self::$instance === null) {
|
||||
try {
|
||||
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",
|
||||
self::$config['host'],
|
||||
self::$config['name']
|
||||
);
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
self::$instance = new PDO(
|
||||
$dsn,
|
||||
self::$config['username'],
|
||||
self::$config['password'],
|
||||
$options
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
22
api/src/Core/Request.php
Normal file
22
api/src/Core/Request.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class Request {
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getJson(): array {
|
||||
$json = file_get_contents('php://input');
|
||||
$data = json_decode($json, true) ?? [];
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new RuntimeException('Invalid JSON payload');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function getValue(string $key, mixed $default = null): mixed {
|
||||
return $_REQUEST[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
97
api/src/Core/Response.php
Normal file
97
api/src/Core/Response.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class Response {
|
||||
public static function json(array $data, int $status = 200): void {
|
||||
// Nettoyer tout buffer existant
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Headers CORS pour permettre les requêtes cross-origin (applications mobiles)
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
|
||||
|
||||
// Configurer les headers CORS
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token, Authorization, X-Requested-With, X-App-Identifier, X-Client-Type');
|
||||
header('Access-Control-Expose-Headers: Content-Length, X-Kuma-Revision');
|
||||
|
||||
// Définir les headers de réponse
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// Définir le code de statut
|
||||
http_response_code($status);
|
||||
|
||||
// Ajouter status et message à la réponse si non présents
|
||||
if (!isset($data['status'])) {
|
||||
if ($status >= 200 && $status < 300) {
|
||||
$data['status'] = 'success';
|
||||
} else {
|
||||
$data['status'] = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($data['message']) && isset($data['error'])) {
|
||||
$data['message'] = $data['error'];
|
||||
}
|
||||
|
||||
// Sanitize data to ensure valid UTF-8 before encoding
|
||||
$sanitizedData = self::sanitizeForJson($data);
|
||||
|
||||
// Encoder et envoyer la réponse
|
||||
$jsonResponse = json_encode($sanitizedData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
// Vérifier si l'encodage a échoué
|
||||
if ($jsonResponse === false) {
|
||||
error_log('Erreur d\'encodage JSON: ' . json_last_error_msg());
|
||||
$jsonResponse = json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur d\'encodage de la réponse',
|
||||
'debug_info' => json_last_error_msg()
|
||||
]);
|
||||
}
|
||||
|
||||
// Log de débogage
|
||||
error_log('Envoi de la réponse JSON: ' . $jsonResponse);
|
||||
|
||||
// Envoyer la réponse
|
||||
echo $jsonResponse;
|
||||
|
||||
// S'assurer que tout est envoyé
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize data recursively to ensure valid UTF-8 encoding for JSON
|
||||
*
|
||||
* @param mixed $data The data to sanitize
|
||||
* @return mixed The sanitized data
|
||||
*/
|
||||
private static function sanitizeForJson($data) {
|
||||
if (is_string($data)) {
|
||||
// Replace invalid UTF-8 characters
|
||||
if (!mb_check_encoding($data, 'UTF-8')) {
|
||||
// Try to convert from other encodings
|
||||
$encodings = ['ISO-8859-1', 'Windows-1252'];
|
||||
foreach ($encodings as $encoding) {
|
||||
$converted = mb_convert_encoding($data, 'UTF-8', $encoding);
|
||||
if (mb_check_encoding($converted, 'UTF-8')) {
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
// If conversion fails, strip invalid characters
|
||||
return mb_convert_encoding($data, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
return $data;
|
||||
} else if (is_array($data)) {
|
||||
// Recursively sanitize array elements
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = self::sanitizeForJson($value);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
208
api/src/Core/Router.php
Normal file
208
api/src/Core/Router.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Router {
|
||||
// Préfixe fixe de l'API (toujours 'api')
|
||||
private const API_PREFIX = 'api';
|
||||
private array $routes = [];
|
||||
private array $publicEndpoints = [
|
||||
'login',
|
||||
'register',
|
||||
'lostpassword',
|
||||
'log',
|
||||
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
// Pas besoin de récupérer AppConfig puisque nous utilisons une constante pour le préfixe API
|
||||
$this->configureRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure toutes les routes de l'application
|
||||
*/
|
||||
private function configureRoutes(): void {
|
||||
// Routes publiques
|
||||
$this->post('login', ['LoginController', 'login']);
|
||||
$this->post('register', ['LoginController', 'register']);
|
||||
$this->post('lostpassword', ['LoginController', 'lostPassword']);
|
||||
|
||||
// Route pour les logs
|
||||
$this->post('log', ['LogController', 'index']);
|
||||
|
||||
// Routes privées utilisateurs
|
||||
$this->get('users', ['UserController', 'getUsers']);
|
||||
$this->get('users/:id', ['UserController', 'getUserById']);
|
||||
$this->post('users', ['UserController', 'createUser']);
|
||||
$this->put('users/:id', ['UserController', 'updateUser']);
|
||||
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
||||
$this->post('logout', ['LoginController', 'logout']);
|
||||
|
||||
// Routes entités
|
||||
$this->get('entites', ['EntiteController', 'getEntites']);
|
||||
$this->get('entites/:id', ['EntiteController', 'getEntiteById']);
|
||||
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
|
||||
|
||||
// Routes villes
|
||||
$this->get('villes', ['VilleController', 'searchVillesByPostalCode']);
|
||||
}
|
||||
|
||||
public function handle(): void {
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = $this->normalizeUri($_SERVER['REQUEST_URI']);
|
||||
|
||||
error_log("Initial URI: $uri");
|
||||
|
||||
// Handle CORS preflight
|
||||
if ($method === 'OPTIONS') {
|
||||
header('HTTP/1.1 200 OK');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Prendre le préfixe API à partir de la constante
|
||||
$apiPrefix = self::API_PREFIX;
|
||||
|
||||
// Vérifier si l'URI commence bien par le préfixe API
|
||||
$prefixMatch = strpos($uri, $apiPrefix) === 0;
|
||||
|
||||
if (!$prefixMatch) {
|
||||
Response::json([
|
||||
'error' => 'Invalid API prefix',
|
||||
'path' => $uri,
|
||||
'expected_prefix' => $apiPrefix
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extraire l'endpoint en retirant le préfixe API
|
||||
$endpoint = substr($uri, strlen($apiPrefix) + 1); // +1 pour le slash
|
||||
$endpoint = trim($endpoint, '/');
|
||||
|
||||
// Check if endpoint is public
|
||||
if ($this->isPublicEndpoint($endpoint)) {
|
||||
error_log("Public endpoint found: $endpoint");
|
||||
$route = $this->findRoute($method, $endpoint);
|
||||
if ($route) {
|
||||
$this->executeRoute($route);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
error_log("Private endpoint: $endpoint");
|
||||
// Private route - check auth first
|
||||
Session::requireAuth();
|
||||
|
||||
$route = $this->findRoute($method, $endpoint);
|
||||
if ($route) {
|
||||
$this->executeRoute($route);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No route found
|
||||
Response::json([
|
||||
'error' => 'Route not found',
|
||||
'endpoint' => $endpoint,
|
||||
'uri' => $uri
|
||||
], 404);
|
||||
}
|
||||
|
||||
private function normalizeUri(string $uri): string {
|
||||
return trim(preg_replace('#/+#', '/', parse_url($uri, PHP_URL_PATH)), '/');
|
||||
}
|
||||
|
||||
private function isPublicEndpoint(string $endpoint): bool {
|
||||
return in_array($endpoint, $this->publicEndpoints);
|
||||
}
|
||||
|
||||
private function executeRoute(array $route): void {
|
||||
[$controllerName, $method] = $route['handler'];
|
||||
|
||||
// Essayer de trouver le contrôleur en tenant compte des namespaces possibles
|
||||
$classNames = [
|
||||
$controllerName, // Sans namespace
|
||||
"\\App\\Controllers\\$controllerName", // Avec namespace complet
|
||||
"\\$controllerName" // Avec namespace racine
|
||||
];
|
||||
|
||||
$controllerClass = null;
|
||||
foreach ($classNames as $className) {
|
||||
if (class_exists($className)) {
|
||||
$controllerClass = $className;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($controllerClass === null) {
|
||||
// Classe non trouvée, gérer l'erreur
|
||||
Response::json([
|
||||
'error' => 'Controller not found',
|
||||
'controller' => $controllerName,
|
||||
'status' => 'error',
|
||||
'message' => 'Controller not found',
|
||||
'tried_namespaces' => implode(', ', $classNames)
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$controller = new $controllerClass();
|
||||
|
||||
if (!empty($route['params'])) {
|
||||
$controller->$method(...$route['params']);
|
||||
} else {
|
||||
$controller->$method();
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $path, array $handler): void {
|
||||
$this->addRoute('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post(string $path, array $handler): void {
|
||||
$this->addRoute('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put(string $path, array $handler): void {
|
||||
$this->addRoute('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete(string $path, array $handler): void {
|
||||
$this->addRoute('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, array $handler): void {
|
||||
// Normalize the path
|
||||
$path = trim($path, '/');
|
||||
$this->routes[$method][$path] = $handler;
|
||||
}
|
||||
|
||||
private function findRoute(string $method, string $uri): ?array {
|
||||
if (!isset($this->routes[$method])) {
|
||||
error_log("Méthode $method non trouvée dans les routes");
|
||||
return null;
|
||||
}
|
||||
|
||||
$uri = trim($uri, '/');
|
||||
error_log("Recherche de route pour: méthode=$method, uri=$uri");
|
||||
error_log("Routes disponibles pour $method: " . implode(', ', array_keys($this->routes[$method])));
|
||||
|
||||
foreach ($this->routes[$method] as $route => $handler) {
|
||||
$pattern = preg_replace('/{[^}]+}/', '([^/]+)', $route);
|
||||
$pattern = "@^" . $pattern . "$@D";
|
||||
error_log("Test pattern: $pattern contre uri: $uri");
|
||||
|
||||
if (preg_match($pattern, $uri, $matches)) {
|
||||
error_log("Route trouvée! Pattern: $pattern, Handler: {$handler[0]}::{$handler[1]}");
|
||||
array_shift($matches);
|
||||
return [
|
||||
'handler' => $handler,
|
||||
'params' => $matches
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
error_log("Aucune route trouvée pour $method $uri");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
135
api/src/Core/Session.php
Normal file
135
api/src/Core/Session.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Session {
|
||||
public static function start(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Configuration des sessions adaptée pour les applications mobiles
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
|
||||
// Permettre les connexions non-HTTPS en développement
|
||||
$isProduction = (getenv('APP_ENV') === 'production');
|
||||
ini_set('session.cookie_secure', $isProduction ? '1' : '0');
|
||||
|
||||
// SameSite None pour permettre les requêtes cross-origin (applications mobiles)
|
||||
ini_set('session.cookie_samesite', 'None');
|
||||
ini_set('session.gc_maxlifetime', '86400'); // 24 heures
|
||||
|
||||
// Récupérer le session_id du Bearer token si présent
|
||||
self::getSessionFromBearer();
|
||||
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
public static function login(array $userData): void {
|
||||
$_SESSION['user_id'] = $userData['id'];
|
||||
$_SESSION['user_email'] = $userData['email'] ?? '';
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
// Régénère l'ID de session pour éviter la fixation de session
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
public static function isAuthenticated(): bool {
|
||||
return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
|
||||
}
|
||||
|
||||
public static function getUserId(): ?int {
|
||||
return $_SESSION['user_id'] ?? null;
|
||||
}
|
||||
|
||||
public static function getUserEmail(): ?string {
|
||||
return $_SESSION['user_email'] ?? null;
|
||||
}
|
||||
|
||||
public static function requireAuth(): void {
|
||||
if (!self::isAuthenticated()) {
|
||||
// Log détaillé pour le debug
|
||||
$logFile = __DIR__ . '/../../logs/auth_' . date('Y-m-d') . '.log';
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? 'No Authorization header';
|
||||
$appId = isset($_SERVER['HTTP_X_APP_IDENTIFIER']) ? $_SERVER['HTTP_X_APP_IDENTIFIER'] : 'No App Identifier';
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'Unknown Method';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? 'Unknown URI';
|
||||
|
||||
$logMessage = "\n===== AUTHENTICATION FAILURE =====\n";
|
||||
$logMessage .= "Date: " . date('Y-m-d H:i:s') . "\n";
|
||||
$logMessage .= "Method: $method\n";
|
||||
$logMessage .= "URI: $uri\n";
|
||||
$logMessage .= "App ID: $appId\n";
|
||||
$logMessage .= "Auth Header: $authHeader\n";
|
||||
$logMessage .= "Session data: " . (isset($_SESSION) ? json_encode($_SESSION) : 'No session') . "\n";
|
||||
$logMessage .= "================================\n";
|
||||
|
||||
file_put_contents($logFile, $logMessage, FILE_APPEND);
|
||||
|
||||
Response::json(['error' => 'Non authentifié - Veuillez vous connecter'], 401);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification optionnelle de l'activité
|
||||
public static function checkActivity(): void {
|
||||
$inactiveTime = 3600; // 1 heure
|
||||
if (
|
||||
isset($_SESSION['last_activity']) &&
|
||||
(time() - $_SESSION['last_activity'] > $inactiveTime)
|
||||
) {
|
||||
self::logout();
|
||||
Response::json(['error' => 'Session expirée'], 440);
|
||||
exit;
|
||||
}
|
||||
$_SESSION['last_activity'] = time();
|
||||
}
|
||||
|
||||
// Récupère le session_id du Bearer token et le définit comme session_id courant
|
||||
private static function getSessionFromBearer(): void {
|
||||
// Vérifier si le header Authorization est présent
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
|
||||
// Mettre toutes les erreurs dans un fichier de log dédié
|
||||
$logFile = __DIR__ . '/../../logs/session_' . date('Y-m-d') . '.log';
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Auth Header: " . $authHeader . "\n", FILE_APPEND);
|
||||
|
||||
// Nettoyage du header d'autorisation
|
||||
$authHeader = trim($authHeader);
|
||||
|
||||
// Support de plusieurs formats possibles
|
||||
if (strpos($authHeader, 'Bearer ') === 0) {
|
||||
// Format standard "Bearer token"
|
||||
$sessionId = substr($authHeader, 7);
|
||||
} elseif (strpos(strtolower($authHeader), 'bearer ') === 0) {
|
||||
// Cas insensible à la casse
|
||||
$sessionId = substr($authHeader, 7);
|
||||
} elseif (preg_match('/^bearer\s+(.*)$/i', $authHeader, $matches)) {
|
||||
// Utilisation de l'expression régulière
|
||||
$sessionId = $matches[1];
|
||||
} else {
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - No Bearer token found in Authorization header\n", FILE_APPEND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Nettoyage du token
|
||||
$sessionId = trim($sessionId);
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Session ID extracted: " . $sessionId . "\n", FILE_APPEND);
|
||||
|
||||
// Vérifier que le session_id a un format valide (alphanumerique avec quelques caractères spéciaux)
|
||||
// Attention: les sessions en PHP peuvent contenir des caractères non-alphanumériques
|
||||
// Assouplir les règles de validation si nécessaire
|
||||
if (!empty($sessionId) && strlen($sessionId) <= 128) {
|
||||
// Définir l'ID de session avant de démarrer la session
|
||||
session_id($sessionId);
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Session ID set: " . $sessionId . "\n", FILE_APPEND);
|
||||
} else {
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Invalid session ID format in Bearer token\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user