$val) { if (strcasecmp($key, 'Authorization') === 0) { $authHeader = trim($val); break; } if (strcasecmp($key, 'X-API-KEY') === 0) { return trim($val); } } } if ($authHeader !== null && preg_match('/Bearer\s+(\S+)/i', $authHeader, $matches)) { return $matches[1]; } if (!empty($_GET['token'])) { return trim((string)$_GET['token']); } return null; } /** * Vérifie l'authentification et les droits de l'utilisateur */ function authenticate(PDO $db, bool $required = true, ?string $requiredRole = null): ?array { $token = getBearerToken(); if ($token === null) { if ($required) { sendJson(401, [ 'success' => false, 'error' => "Authentification requise. Veuillez fournir un jeton 'Authorization: Bearer ' ou 'X-API-KEY'." ]); } return null; } $stmt = $db->prepare(' SELECT id, username, nom, role, api_token, token_expires_at FROM users WHERE api_token = ? LIMIT 1 '); $stmt->execute([$token]); $user = $stmt->fetch(); if (!$user) { sendJson(401, [ 'success' => false, 'error' => "Jeton d'authentification invalide. Veuillez vous reconnecter via POST /auth/login." ]); } if (!empty($user['token_expires_at']) && strtotime($user['token_expires_at']) < time()) { sendJson(401, [ 'success' => false, 'error' => "Jeton d'authentification expiré. Veuillez vous reconnecter via POST /auth/login." ]); } if ($requiredRole !== null && strtolower((string)$user['role']) !== strtolower($requiredRole)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Privilèges insuffisants (rôle '$requiredRole' requis, rôle actuel : '{$user['role']}')." ]); } return $user; } /** * Formate une ligne de voiture avec son propriétaire */ function formatCarRow(array $row): array { $row['id'] = (int)$row['id']; $row['annee'] = (int)$row['annee']; $row['dernier_km'] = isset($row['dernier_km']) ? (int)$row['dernier_km'] : null; $row['proprietaire'] = null; if (!empty($row['user_id'])) { $row['proprietaire'] = [ 'id' => (int)$row['user_id'], 'username' => $row['proprietaire_username'] ?? null, 'nom' => $row['proprietaire_nom'] ?? null, ]; } unset($row['proprietaire_username'], $row['proprietaire_nom']); return $row; } /** * Récupère une voiture par son ID avec les informations du propriétaire */ function getCarById(PDO $db, int $carId): ?array { $stmt = $db->prepare(' SELECT c.*, u.username AS proprietaire_username, u.nom AS proprietaire_nom, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km FROM cars c LEFT JOIN users u ON c.user_id = u.id WHERE c.id = ? '); $stmt->execute([$carId]); $car = $stmt->fetch(); return $car ? formatCarRow($car) : null; } /** * Vérifie si l'utilisateur est le propriétaire de la voiture ou un administrateur */ function canModifyCar(array $car, array $currentUser): bool { if (strtolower((string)$currentUser['role']) === 'admin') { return true; } return !empty($car['user_id']) && (int)$car['user_id'] === (int)$currentUser['id']; } /** * Recherche d'une voiture par sa plaque d'immatriculation */ function findCarByPlate(PDO $db, string $plaque): void { $clean = strtoupper(trim(urldecode($plaque))); $norm = str_replace(['-', ' ', '.', '_'], '', $clean); $stmt = $db->prepare(' SELECT c.*, u.username AS proprietaire_username, u.nom AS proprietaire_nom, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km FROM cars c LEFT JOIN users u ON c.user_id = u.id WHERE UPPER(c.immatriculation) = :raw OR REPLACE(REPLACE(REPLACE(REPLACE(UPPER(c.immatriculation), "-", ""), " ", ""), ".", ""), "_", "") = :norm LIMIT 1 '); $stmt->execute([':raw' => $clean, ':norm' => $norm]); $car = $stmt->fetch(); if (!$car) { sendJson(404, [ 'success' => false, 'error' => "Aucune voiture trouvée avec l'immatriculation '$clean'" ]); } sendJson(200, [ 'success' => true, 'data' => formatCarRow($car) ]); } try { $db = getDB(); $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); $segments = getPathSegments(); // Route racine: Documentation des points d'accès if (empty($segments)) { sendJson(200, [ 'success' => true, 'message' => 'API Voitures avec Gestion des Propriétaires & Authentification opérationnelle', 'database' => DB_NAME, 'comptes_demo' => [ 'admin' => ['username' => 'admin', 'password' => 'adminpassword', 'role' => 'admin', 'token_fixe' => 'admin-token-secret-12345'], 'user' => ['username' => 'user', 'password' => 'userpassword', 'role' => 'user', 'token_fixe' => 'user-token-secret-67890'] ], 'regles_droits' => [ 'Admins' => 'Accès total à toutes les voitures, détails confidentiels, modifications et suppressions.', 'Propriétaires' => 'Peuvent modifier/supprimer leurs propres voitures et gérer leurs entretiens, kilométrages et notes.', 'Public' => 'Consultation de la fiche d\'une voiture par son identifiant (/cars/{id}).' ], 'endpoints' => [ 'Authentification' => [ 'POST /auth/login' => '[PUBLIC] Se connecter et obtenir un jeton (username, password)', 'POST /auth/register' => '[PUBLIC] Créer un compte utilisateur (username, password, nom)', 'GET /auth/me' => '[AUTHENTIFIÉ] Obtenir son profil et son rôle', 'POST /auth/logout' => '[AUTHENTIFIÉ] Révoquer le jeton de session' ], 'Voitures' => [ 'GET /cars' => '[ADMIN] Liste de toutes les voitures (filtrable par ?immatriculation=..., ?user_id=..., ?mine=true)', 'GET /cars/mes-voitures' => '[AUTHENTIFIÉ] Liste uniquement les voitures de l\'utilisateur connecté', 'GET /cars/{id}' => '[PUBLIC] Détails d\'une voiture avec son propriétaire', 'GET /cars/immatriculation/{plaque}' => '[ADMIN] Identifier une voiture par sa plaque', 'GET /cars/{id}/etat' => '[PROPRIÉTAIRE ou ADMIN] État complet (voiture, dernier km, entretiens, notes)', 'POST /cars' => '[AUTHENTIFIÉ] Créer une voiture (attribuée automatiquement à l\'utilisateur connecté)', 'PUT /cars/{id}' => '[PROPRIÉTAIRE ou ADMIN] Modifier les paramètres d\'une voiture', 'DELETE /cars/{id}' => '[PROPRIÉTAIRE ou ADMIN] Supprimer une voiture et toutes ses données associées' ], 'Kilométrage' => [ 'GET /cars/{id}/kilometrage' => '[PUBLIC] Historique des relevés kilométriques', 'POST /cars/{id}/kilometrage' => '[PROPRIÉTAIRE ou ADMIN] Ajouter un relevé kilométrique', 'DELETE /kilometrage/{id}' => '[PROPRIÉTAIRE ou ADMIN] Supprimer un relevé kilométrique' ], 'Entretiens' => [ 'GET /cars/{id}/maintenance' => '[PUBLIC] Historique des entretiens d\'une voiture', 'GET /maintenance/{id}' => '[PUBLIC] Détails d\'un entretien', 'POST /cars/{id}/maintenance' => '[PROPRIÉTAIRE ou ADMIN] Ajouter un entretien', 'PUT /maintenance/{id}' => '[PROPRIÉTAIRE ou ADMIN] Modifier un entretien', 'DELETE /maintenance/{id}' => '[PROPRIÉTAIRE ou ADMIN] Supprimer un entretien' ], 'Notes (Confidentielles)' => [ 'GET /cars/{id}/notes' => '[PROPRIÉTAIRE ou ADMIN] Consulter les notes de suivi', 'POST /cars/{id}/notes' => '[PROPRIÉTAIRE ou ADMIN] Ajouter une note', 'PUT /notes/{id}' => '[PROPRIÉTAIRE ou ADMIN] Modifier une note', 'DELETE /notes/{id}' => '[PROPRIÉTAIRE ou ADMIN] Supprimer une note' ] ] ]); } $resource = strtolower($segments[0]); // ========================================================================= // RESSOURCE: AUTHENTIFICATION (/auth ...) // ========================================================================= if ($resource === 'auth') { $action = strtolower($segments[1] ?? ''); // POST /auth/login if ($action === 'login' && $method === 'POST') { $body = getBody(); if (empty($body['username']) || empty($body['password'])) { sendJson(400, ['success' => false, 'error' => 'Champs username et password obligatoires']); } $stmt = $db->prepare('SELECT * FROM users WHERE username = ? LIMIT 1'); $stmt->execute([trim((string)$body['username'])]); $user = $stmt->fetch(); if (!$user || !password_verify((string)$body['password'], $user['password'])) { sendJson(401, ['success' => false, 'error' => 'Identifiants invalides (nom d\'utilisateur ou mot de passe incorrect)']); } $token = bin2hex(random_bytes(32)); $expiresAt = date('Y-m-d H:i:s', time() + (7 * 86400)); $stmtUpdate = $db->prepare('UPDATE users SET api_token = ?, token_expires_at = ? WHERE id = ?'); $stmtUpdate->execute([$token, $expiresAt, $user['id']]); sendJson(200, [ 'success' => true, 'message' => 'Connexion réussie', 'data' => [ 'user' => [ 'id' => (int)$user['id'], 'username' => $user['username'], 'nom' => $user['nom'], 'role' => $user['role'] ], 'token' => $token, 'expires_at' => $expiresAt ] ]); } // POST /auth/register if ($action === 'register' && $method === 'POST') { $body = getBody(); if (empty($body['username']) || empty($body['password'])) { sendJson(400, ['success' => false, 'error' => 'Champs username et password obligatoires']); } $username = trim((string)$body['username']); $nom = !empty($body['nom']) ? trim((string)$body['nom']) : null; $password = password_hash((string)$body['password'], PASSWORD_DEFAULT); $stmtCheck = $db->prepare('SELECT id FROM users WHERE username = ?'); $stmtCheck->execute([$username]); if ($stmtCheck->fetch()) { sendJson(409, ['success' => false, 'error' => "Le nom d'utilisateur '$username' est déjà utilisé"]); } $token = bin2hex(random_bytes(32)); $expiresAt = date('Y-m-d H:i:s', time() + (7 * 86400)); $stmt = $db->prepare('INSERT INTO users (username, password, nom, role, api_token, token_expires_at) VALUES (?, ?, ?, ?, ?, ?)'); $stmt->execute([$username, $password, $nom, 'user', $token, $expiresAt]); $newId = (int)$db->lastInsertId(); sendJson(201, [ 'success' => true, 'message' => 'Compte créé avec succès', 'data' => [ 'user' => [ 'id' => $newId, 'username' => $username, 'nom' => $nom, 'role' => 'user' ], 'token' => $token, 'expires_at' => $expiresAt ] ]); } // GET /auth/me if ($action === 'me' && $method === 'GET') { $user = authenticate($db, true); sendJson(200, [ 'success' => true, 'data' => [ 'id' => (int)$user['id'], 'username' => $user['username'], 'nom' => $user['nom'], 'role' => $user['role'], 'token_expires_at' => $user['token_expires_at'] ] ]); } // POST /auth/logout if ($action === 'logout' && $method === 'POST') { $user = authenticate($db, true); $stmt = $db->prepare('UPDATE users SET api_token = NULL, token_expires_at = NULL WHERE id = ?'); $stmt->execute([$user['id']]); sendJson(200, ['success' => true, 'message' => 'Déconnexion réussie, le jeton a été révoqué']); } sendJson(404, ['success' => false, 'error' => "Action d'authentification '$action' non reconnue"]); } // ========================================================================= // ENDPOINT DIRECT: /immatriculation/{plaque} [ADMIN REQUIS] // ========================================================================= if ($resource === 'immatriculation' && $method === 'GET') { authenticate($db, true, 'admin'); $plaque = $segments[1] ?? null; if (empty($plaque)) { sendJson(400, ['success' => false, 'error' => 'Numéro d\'immatriculation manquant dans l\'URL']); } findCarByPlate($db, $plaque); } // ========================================================================= // RESSOURCE: CARS (/cars ...) // ========================================================================= if ($resource === 'cars' || $resource === 'voitures') { // GET /cars/mes-voitures : Liste des voitures de l'utilisateur connecté if (isset($segments[1]) && (strtolower($segments[1]) === 'mes-voitures' || strtolower($segments[1]) === 'mine') && $method === 'GET') { $currentUser = authenticate($db, true); $stmt = $db->prepare(' SELECT c.*, u.username AS proprietaire_username, u.nom AS proprietaire_nom, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km FROM cars c LEFT JOIN users u ON c.user_id = u.id WHERE c.user_id = ? ORDER BY c.id ASC '); $stmt->execute([$currentUser['id']]); $list = array_map('formatCarRow', $stmt->fetchAll()); sendJson(200, ['success' => true, 'data' => $list]); } // GET /cars/immatriculation/{plaque} [ADMIN REQUIS] if (isset($segments[1]) && strtolower($segments[1]) === 'immatriculation' && $method === 'GET') { authenticate($db, true, 'admin'); $plaque = $segments[2] ?? null; if (empty($plaque)) { sendJson(400, ['success' => false, 'error' => 'Numéro d\'immatriculation manquant dans l\'URL']); } findCarByPlate($db, $plaque); } $carId = isset($segments[1]) && is_numeric($segments[1]) ? (int)$segments[1] : null; $subResource = $segments[2] ?? null; // 1. GET /cars/{id}/etat [PROPRIÉTAIRE OU ADMIN REQUIS] if ($carId && $subResource === 'etat' && $method === 'GET') { $currentUser = authenticate($db, true); $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } if (!canModifyCar($car, $currentUser)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Les administrateurs et le propriétaire du véhicule sont les seuls autorisés à consulter son état complet." ]); } try { $stmt = $db->prepare('CALL sp_getEtatVoiture(:id)'); $stmt->execute([':id' => $carId]); $carRow = $stmt->fetch(); $maintenances = []; if ($stmt->nextRowset()) { $maintenances = $stmt->fetchAll(); } $notes = []; if ($stmt->nextRowset()) { $notes = $stmt->fetchAll(); } $stmt->closeCursor(); sendJson(200, [ 'success' => true, 'consulted_by' => [ 'username' => $currentUser['username'], 'role' => $currentUser['role'] ], 'data' => [ 'voiture' => $car, 'maintenances' => $maintenances, 'notes' => $notes ] ]); } catch (Exception $e) { $stmtMaint = $db->prepare('SELECT * FROM maintenance_logs WHERE car_id = ? ORDER BY date_evenement DESC'); $stmtMaint->execute([$carId]); $stmtNotes = $db->prepare('SELECT * FROM notes WHERE car_id = ? ORDER BY date_creation DESC'); $stmtNotes->execute([$carId]); sendJson(200, [ 'success' => true, 'consulted_by' => [ 'username' => $currentUser['username'], 'role' => $currentUser['role'] ], 'data' => [ 'voiture' => $car, 'maintenances' => $stmtMaint->fetchAll(), 'notes' => $stmtNotes->fetchAll() ] ]); } } // 2. /cars/{id}/kilometrage if ($carId && $subResource === 'kilometrage') { // GET: Public if ($method === 'GET') { $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 50; try { $stmt = $db->prepare('CALL sp_getKilometrages(:id, :limit)'); $stmt->execute([':id' => $carId, ':limit' => $limit]); $data = $stmt->fetchAll(); $stmt->closeCursor(); } catch (Exception $e) { $stmt = $db->prepare('SELECT * FROM kilometrage WHERE car_id = ? ORDER BY date_releve DESC, id DESC LIMIT ?'); $stmt->bindValue(1, $carId, PDO::PARAM_INT); $stmt->bindValue(2, $limit, PDO::PARAM_INT); $stmt->execute(); $data = $stmt->fetchAll(); } sendJson(200, ['success' => true, 'data' => $data]); } // POST: [PROPRIÉTAIRE OU ADMIN REQUIS] if ($method === 'POST') { $currentUser = authenticate($db, true); $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } if (!canModifyCar($car, $currentUser)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Seul le propriétaire ou un administrateur peut enregistrer un relevé kilométrique pour cette voiture." ]); } $body = getBody(); if (!isset($body['valeur'])) { sendJson(400, ['success' => false, 'error' => 'Le champ valeur (kilométrage) est obligatoire']); } $dateReleve = $body['date_releve'] ?? date('Y-m-d'); $valeur = (int)$body['valeur']; $stmt = $db->prepare('INSERT INTO kilometrage (car_id, date_releve, valeur) VALUES (?, ?, ?)'); $stmt->execute([$carId, $dateReleve, $valeur]); $newId = (int)$db->lastInsertId(); sendJson(201, [ 'success' => true, 'message' => 'Relevé kilométrique enregistré', 'data' => [ 'id' => $newId, 'car_id' => $carId, 'date_releve' => $dateReleve, 'valeur' => $valeur ] ]); } sendJson(405, ['success' => false, 'error' => 'Méthode non autorisée pour cette sous-ressource']); } // 3. /cars/{id}/maintenance if ($carId && ($subResource === 'maintenance' || $subResource === 'entretiens')) { // GET: Public if ($method === 'GET') { $stmt = $db->prepare('SELECT * FROM maintenance_logs WHERE car_id = ? ORDER BY date_evenement DESC'); $stmt->execute([$carId]); sendJson(200, ['success' => true, 'data' => $stmt->fetchAll()]); } // POST: [PROPRIÉTAIRE OU ADMIN REQUIS] if ($method === 'POST') { $currentUser = authenticate($db, true); $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } if (!canModifyCar($car, $currentUser)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Seul le propriétaire ou un administrateur peut ajouter un entretien pour cette voiture." ]); } $body = getBody(); if (empty($body['type_entretien']) || empty($body['date_evenement']) || !isset($body['kilometrage'])) { sendJson(400, [ 'success' => false, 'error' => 'Les champs type_entretien, date_evenement et kilometrage sont obligatoires' ]); } $stmt = $db->prepare(' INSERT INTO maintenance_logs (car_id, type_entretien, date_evenement, kilometrage, description, prix) VALUES (?, ?, ?, ?, ?, ?) '); $stmt->execute([ $carId, trim((string)$body['type_entretien']), (string)$body['date_evenement'], (int)$body['kilometrage'], isset($body['description']) ? trim((string)$body['description']) : null, isset($body['prix']) ? (float)$body['prix'] : null ]); $newId = (int)$db->lastInsertId(); $stmtGet = $db->prepare('SELECT * FROM maintenance_logs WHERE id = ?'); $stmtGet->execute([$newId]); sendJson(201, [ 'success' => true, 'message' => 'Entretien enregistré avec succès', 'data' => $stmtGet->fetch() ]); } sendJson(405, ['success' => false, 'error' => 'Méthode non autorisée pour cette sous-ressource']); } // 4. /cars/{id}/notes [PROPRIÉTAIRE OU ADMIN REQUIS] if ($carId && $subResource === 'notes') { $currentUser = authenticate($db, true); $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } if (!canModifyCar($car, $currentUser)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Seul le propriétaire ou un administrateur peut consulter ou ajouter des notes pour cette voiture." ]); } // GET: Notes if ($method === 'GET') { $stmt = $db->prepare('SELECT * FROM notes WHERE car_id = ? ORDER BY date_creation DESC'); $stmt->execute([$carId]); sendJson(200, ['success' => true, 'data' => $stmt->fetchAll()]); } // POST: Ajouter une note if ($method === 'POST') { $body = getBody(); if (empty($body['contenu'])) { sendJson(400, ['success' => false, 'error' => 'Le champ contenu est obligatoire']); } $titre = isset($body['titre']) ? trim((string)$body['titre']) : null; $contenu = trim((string)$body['contenu']); $stmt = $db->prepare('INSERT INTO notes (car_id, titre, contenu) VALUES (?, ?, ?)'); $stmt->execute([$carId, $titre, $contenu]); $newId = (int)$db->lastInsertId(); $stmtGet = $db->prepare('SELECT * FROM notes WHERE id = ?'); $stmtGet->execute([$newId]); sendJson(201, [ 'success' => true, 'message' => 'Note enregistrée avec succès', 'data' => $stmtGet->fetch() ]); } sendJson(405, ['success' => false, 'error' => 'Méthode non autorisée pour cette sous-ressource']); } // --- ACTIONS PRINCIPALES SUR LES VOITURES --- // GET /cars : Liste toutes les voitures [ADMIN REQUIS] if ($carId === null && $subResource === null && $method === 'GET') { $currentUser = authenticate($db, true, 'admin'); // Filtre par immatriculation if (!empty($_GET['immatriculation'])) { findCarByPlate($db, (string)$_GET['immatriculation']); } $where = []; $params = []; // Filtre mes voitures (?mine=true) if (!empty($_GET['mine'])) { $where[] = 'c.user_id = ?'; $params[] = $currentUser['id']; } elseif (!empty($_GET['user_id'])) { $where[] = 'c.user_id = ?'; $params[] = (int)$_GET['user_id']; } $sql = ' SELECT c.*, u.username AS proprietaire_username, u.nom AS proprietaire_nom, (SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km FROM cars c LEFT JOIN users u ON c.user_id = u.id '; if (!empty($where)) { $sql .= ' WHERE ' . implode(' AND ', $where); } $sql .= ' ORDER BY c.id ASC'; $stmt = $db->prepare($sql); $stmt->execute($params); $rows = array_map('formatCarRow', $stmt->fetchAll()); sendJson(200, ['success' => true, 'data' => $rows]); } // GET /cars/{id} : Consulter une voiture (Public) if ($carId !== null && $subResource === null && $method === 'GET') { $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } sendJson(200, ['success' => true, 'data' => $car]); } // POST /cars : Créer une voiture [AUTHENTIFIÉ REQUIS - ATTRIBUTION DU USER_ID] if ($carId === null && $method === 'POST') { $currentUser = authenticate($db, true); $body = getBody(); $required = ['marque', 'modele', 'annee', 'dateAchat']; foreach ($required as $field) { if (empty($body[$field])) { sendJson(400, ['success' => false, 'error' => "Le champ '$field' est obligatoire"]); } } $marque = trim((string)$body['marque']); $modele = trim((string)$body['modele']); $annee = (int)$body['annee']; $dateAchat = (string)$body['dateAchat']; $vin = !empty($body['VIN']) ? trim((string)$body['VIN']) : null; $immatriculation = !empty($body['immatriculation']) ? strtoupper(trim((string)$body['immatriculation'])) : null; // Détermination du propriétaire (user_id): // Si l'utilisateur est admin et spécifie un user_id, on l'attribue à cet utilisateur; sinon c'est l'utilisateur connecté $targetUserId = (int)$currentUser['id']; if (strtolower((string)$currentUser['role']) === 'admin' && !empty($body['user_id'])) { $targetUserId = (int)$body['user_id']; $chkUser = $db->prepare('SELECT id FROM users WHERE id = ?'); $chkUser->execute([$targetUserId]); if (!$chkUser->fetch()) { sendJson(400, ['success' => false, 'error' => "L'utilisateur #$targetUserId spécifié est introuvable."]); } } // Vérifier unicité de la plaque if ($immatriculation !== null) { $normImmat = str_replace(['-', ' ', '.', '_'], '', $immatriculation); $stmtCheck = $db->prepare(' SELECT id FROM cars WHERE UPPER(immatriculation) = :raw OR REPLACE(REPLACE(REPLACE(REPLACE(UPPER(immatriculation), "-", ""), " ", ""), ".", ""), "_", "") = :norm '); $stmtCheck->execute([':raw' => $immatriculation, ':norm' => $normImmat]); if ($stmtCheck->fetch()) { sendJson(409, [ 'success' => false, 'error' => "L'immatriculation '$immatriculation' est déjà utilisée pour un autre véhicule." ]); } } // Procédure stockée sp_insertVoiture avec user_id try { $stmt = $db->prepare('CALL sp_insertVoiture(:user_id, :marque, :modele, :annee, :dateAchat, :vin, :immatriculation)'); $stmt->execute([ ':user_id' => $targetUserId, ':marque' => $marque, ':modele' => $modele, ':annee' => $annee, ':dateAchat' => $dateAchat, ':vin' => $vin, ':immatriculation' => $immatriculation ]); $res = $stmt->fetch(); $stmt->closeCursor(); $newId = isset($res['new_id']) ? (int)$res['new_id'] : (int)$db->lastInsertId(); } catch (Exception $e) { $stmt = $db->prepare('INSERT INTO cars (user_id, marque, modele, annee, dateAchat, VIN, immatriculation) VALUES (?, ?, ?, ?, ?, ?, ?)'); $stmt->execute([$targetUserId, $marque, $modele, $annee, $dateAchat, $vin, $immatriculation]); $newId = (int)$db->lastInsertId(); } if (!empty($body['kilometrage_initial'])) { $kmStmt = $db->prepare('INSERT INTO kilometrage (car_id, date_releve, valeur) VALUES (?, ?, ?)'); $kmStmt->execute([$newId, $dateAchat, (int)$body['kilometrage_initial']]); } $createdCar = getCarById($db, $newId); sendJson(201, [ 'success' => true, 'message' => 'Voiture créée avec succès', 'data' => $createdCar ]); } // PUT / PATCH /cars/{id} : Mettre à jour les paramètres [SEUL LE PROPRIÉTAIRE OU ADMIN] if ($carId !== null && $subResource === null && ($method === 'PUT' || $method === 'PATCH')) { $currentUser = authenticate($db, true); $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } // Vérification de la propriété if (!canModifyCar($car, $currentUser)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Seul le propriétaire de la voiture ou un administrateur est autorisé à la modifier." ]); } $body = getBody(); $allowedFields = ['marque', 'modele', 'annee', 'dateAchat', 'VIN', 'immatriculation']; // Seul l'admin peut réassigner le user_id de la voiture if (strtolower((string)$currentUser['role']) === 'admin' && array_key_exists('user_id', $body)) { $allowedFields[] = 'user_id'; } $updates = []; $params = []; if (!empty($body['immatriculation'])) { $immat = strtoupper(trim((string)$body['immatriculation'])); $body['immatriculation'] = $immat; $normImmat = str_replace(['-', ' ', '.', '_'], '', $immat); $stmtCheck = $db->prepare(' SELECT id FROM cars WHERE (UPPER(immatriculation) = :raw OR REPLACE(REPLACE(REPLACE(REPLACE(UPPER(immatriculation), "-", ""), " ", ""), ".", ""), "_", "") = :norm) AND id != :id '); $stmtCheck->execute([':raw' => $immat, ':norm' => $normImmat, ':id' => $carId]); if ($stmtCheck->fetch()) { sendJson(409, [ 'success' => false, 'error' => "L'immatriculation '$immat' est déjà utilisée pour un autre véhicule." ]); } } foreach ($allowedFields as $field) { if (array_key_exists($field, $body)) { $updates[] = "`$field` = ?"; $params[] = $body[$field]; } } if (empty($updates)) { sendJson(400, ['success' => false, 'error' => 'Aucun paramètre valide à mettre à jour']); } $params[] = $carId; $sql = 'UPDATE cars SET ' . implode(', ', $updates) . ' WHERE id = ?'; $stmt = $db->prepare($sql); $stmt->execute($params); $updatedCar = getCarById($db, $carId); sendJson(200, [ 'success' => true, 'message' => 'Paramètres de la voiture mis à jour avec succès', 'data' => $updatedCar ]); } // DELETE /cars/{id} : Supprimer une voiture [SEUL LE PROPRIÉTAIRE OU ADMIN] if ($carId !== null && $subResource === null && $method === 'DELETE') { $currentUser = authenticate($db, true); $car = getCarById($db, $carId); if (!$car) { sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]); } // Vérification de la propriété if (!canModifyCar($car, $currentUser)) { sendJson(403, [ 'success' => false, 'error' => "Accès refusé. Seul le propriétaire de la voiture ou un administrateur est autorisé à la supprimer." ]); } try { $stmt = $db->prepare('CALL sp_deleteVoiture(:id)'); $stmt->execute([':id' => $carId]); $stmt->closeCursor(); } catch (Exception $e) { $stmt = $db->prepare('DELETE FROM cars WHERE id = ?'); $stmt->execute([$carId]); } sendJson(200, [ 'success' => true, 'message' => "Voiture #{$carId} ainsi que toutes ses données associées ont été supprimées avec succès" ]); } sendJson(405, ['success' => false, 'error' => "Méthode HTTP non autorisée pour cette URL"]); } // ========================================================================= // RESSOURCE: KILOMETRAGE DIRECT (/kilometrage/{id}) // ========================================================================= if ($resource === 'kilometrage') { $kmId = isset($segments[1]) && is_numeric($segments[1]) ? (int)$segments[1] : null; if ($kmId && $method === 'DELETE') { $currentUser = authenticate($db, true); // Récupérer la voiture liée $stmtKm = $db->prepare('SELECT car_id FROM kilometrage WHERE id = ?'); $stmtKm->execute([$kmId]); $kmRow = $stmtKm->fetch(); if (!$kmRow) { sendJson(404, ['success' => false, 'error' => "Relevé kilométrique #{$kmId} introuvable"]); } $car = getCarById($db, (int)$kmRow['car_id']); if (!canModifyCar($car, $currentUser)) { sendJson(403, ['success' => false, 'error' => "Accès refusé. Seul le propriétaire du véhicule ou un administrateur peut supprimer ce relevé."]); } $stmt = $db->prepare('DELETE FROM kilometrage WHERE id = ?'); $stmt->execute([$kmId]); sendJson(200, ['success' => true, 'message' => "Relevé kilométrique #{$kmId} supprimé"]); } sendJson(405, ['success' => false, 'error' => "Méthode non autorisée sur /kilometrage"]); } // ========================================================================= // RESSOURCE: ENTRETIENS DIRECT (/maintenance/{id}) // ========================================================================= if ($resource === 'maintenance' || $resource === 'entretiens') { $maintId = isset($segments[1]) && is_numeric($segments[1]) ? (int)$segments[1] : null; if (!$maintId) { sendJson(400, ['success' => false, 'error' => 'Identifiant d\'entretien requis']); } if ($method === 'GET') { $stmt = $db->prepare('SELECT * FROM maintenance_logs WHERE id = ?'); $stmt->execute([$maintId]); $item = $stmt->fetch(); if (!$item) { sendJson(404, ['success' => false, 'error' => "Entretien #{$maintId} introuvable"]); } sendJson(200, ['success' => true, 'data' => $item]); } if ($method === 'PUT' || $method === 'PATCH') { $currentUser = authenticate($db, true); $stmtMaint = $db->prepare('SELECT car_id FROM maintenance_logs WHERE id = ?'); $stmtMaint->execute([$maintId]); $maintRow = $stmtMaint->fetch(); if (!$maintRow) { sendJson(404, ['success' => false, 'error' => "Entretien #{$maintId} introuvable"]); } $car = getCarById($db, (int)$maintRow['car_id']); if (!canModifyCar($car, $currentUser)) { sendJson(403, ['success' => false, 'error' => "Accès refusé. Seul le propriétaire du véhicule ou un administrateur peut modifier cet entretien."]); } $body = getBody(); $allowed = ['type_entretien', 'date_evenement', 'kilometrage', 'description', 'prix']; $updates = []; $params = []; foreach ($allowed as $f) { if (array_key_exists($f, $body)) { $updates[] = "`$f` = ?"; $params[] = $body[$f]; } } if (empty($updates)) { sendJson(400, ['success' => false, 'error' => 'Aucun paramètre à modifier']); } $params[] = $maintId; $stmt = $db->prepare('UPDATE maintenance_logs SET ' . implode(', ', $updates) . ' WHERE id = ?'); $stmt->execute($params); $stmtGet = $db->prepare('SELECT * FROM maintenance_logs WHERE id = ?'); $stmtGet->execute([$maintId]); sendJson(200, [ 'success' => true, 'message' => 'Entretien mis à jour avec succès', 'data' => $stmtGet->fetch() ]); } if ($method === 'DELETE') { $currentUser = authenticate($db, true); $stmtMaint = $db->prepare('SELECT car_id FROM maintenance_logs WHERE id = ?'); $stmtMaint->execute([$maintId]); $maintRow = $stmtMaint->fetch(); if (!$maintRow) { sendJson(404, ['success' => false, 'error' => "Entretien #{$maintId} introuvable"]); } $car = getCarById($db, (int)$maintRow['car_id']); if (!canModifyCar($car, $currentUser)) { sendJson(403, ['success' => false, 'error' => "Accès refusé. Seul le propriétaire du véhicule ou un administrateur peut supprimer cet entretien."]); } $stmt = $db->prepare('DELETE FROM maintenance_logs WHERE id = ?'); $stmt->execute([$maintId]); sendJson(200, ['success' => true, 'message' => "Entretien #{$maintId} supprimé avec succès"]); } sendJson(405, ['success' => false, 'error' => "Méthode non autorisée sur /maintenance/{id}"]); } // ========================================================================= // RESSOURCE: NOTES DIRECT (/notes/{id}) [PROPRIÉTAIRE OU ADMIN REQUIS] // ========================================================================= if ($resource === 'notes') { $noteId = isset($segments[1]) && is_numeric($segments[1]) ? (int)$segments[1] : null; if (!$noteId) { sendJson(400, ['success' => false, 'error' => 'Identifiant de note requis']); } $currentUser = authenticate($db, true); $stmtNote = $db->prepare('SELECT car_id, titre, contenu, date_creation FROM notes WHERE id = ?'); $stmtNote->execute([$noteId]); $noteRow = $stmtNote->fetch(); if (!$noteRow) { sendJson(404, ['success' => false, 'error' => "Note #{$noteId} introuvable"]); } $car = getCarById($db, (int)$noteRow['car_id']); if (!canModifyCar($car, $currentUser)) { sendJson(403, ['success' => false, 'error' => "Accès refusé. Seul le propriétaire du véhicule ou un administrateur peut accéder à cette note."]); } // GET: Consulter la note if ($method === 'GET') { sendJson(200, ['success' => true, 'data' => $noteRow]); } // PUT/PATCH: Modifier la note if ($method === 'PUT' || $method === 'PATCH') { $body = getBody(); $allowed = ['titre', 'contenu']; $updates = []; $params = []; foreach ($allowed as $f) { if (array_key_exists($f, $body)) { $updates[] = "`$f` = ?"; $params[] = $body[$f]; } } if (empty($updates)) { sendJson(400, ['success' => false, 'error' => 'Aucun paramètre à modifier']); } $params[] = $noteId; $stmt = $db->prepare('UPDATE notes SET ' . implode(', ', $updates) . ' WHERE id = ?'); $stmt->execute($params); $stmtGet = $db->prepare('SELECT * FROM notes WHERE id = ?'); $stmtGet->execute([$noteId]); sendJson(200, [ 'success' => true, 'message' => 'Note mise à jour avec succès', 'data' => $stmtGet->fetch() ]); } // DELETE: Supprimer la note if ($method === 'DELETE') { $stmt = $db->prepare('DELETE FROM notes WHERE id = ?'); $stmt->execute([$noteId]); sendJson(200, ['success' => true, 'message' => "Note #{$noteId} supprimée avec succès"]); } sendJson(405, ['success' => false, 'error' => "Méthode non autorisée sur /notes/{id}"]); } sendJson(404, [ 'success' => false, 'error' => "Ressource '/$resource' non reconnue. Consultez la racine '/' pour la documentation des routes." ]); } catch (PDOException $e) { sendJson(500, ['success' => false, 'error' => 'Erreur SQL: ' . $e->getMessage()]); } catch (Throwable $e) { sendJson(500, ['success' => false, 'error' => 'Erreur serveur: ' . $e->getMessage()]); }