VoituresApi-API/index.php

960 lines
39 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
// En-têtes HTTP pour API REST JSON & CORS
header('Content-Type: application/json; charset=UTF-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-API-KEY');
// Gestion des requêtes de pré-vérification OPTIONS (CORS)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
/**
* Réponse JSON standardisée
*/
function sendJson(int $statusCode, array $data): void
{
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
/**
* Récupère le corps de la requête (JSON ou FormData)
*/
function getBody(): array
{
$raw = file_get_contents('php://input');
if (!empty($raw)) {
if (!mb_check_encoding($raw, 'UTF-8')) {
$raw = mb_convert_encoding($raw, 'UTF-8', 'ISO-8859-1');
}
$decoded = json_decode($raw, true);
if (is_array($decoded)) {
return $decoded;
}
}
return $_POST ?? [];
}
/**
* Résolution du chemin de la requête
*/
function getPathSegments(): array
{
if (!empty($_GET['route'])) {
$path = trim($_GET['route'], '/');
return $path === '' ? [] : explode('/', $path);
}
if (!empty($_SERVER['PATH_INFO'])) {
$path = trim($_SERVER['PATH_INFO'], '/');
return $path === '' ? [] : explode('/', $path);
}
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$scriptDir = dirname($_SERVER['SCRIPT_NAME'] ?? '');
if ($scriptDir !== '/' && $scriptDir !== '\\' && strpos($uri, $scriptDir) === 0) {
$uri = substr($uri, strlen($scriptDir));
}
$uri = preg_replace('#^/?index\.php#i', '', $uri);
$path = trim($uri, '/');
return $path === '' ? [] : explode('/', $path);
}
/**
* Récupère le jeton Bearer depuis les en-têtes HTTP ou paramètre d'URL
*/
function getBearerToken(): ?string
{
$authHeader = null;
if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
$authHeader = trim($_SERVER['HTTP_AUTHORIZATION']);
} elseif (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$authHeader = trim($_SERVER['REDIRECT_HTTP_AUTHORIZATION']);
} elseif (!empty($_SERVER['HTTP_X_API_KEY'])) {
return trim($_SERVER['HTTP_X_API_KEY']);
} elseif (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
foreach ($headers as $key => $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 <token>' 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."
]);
}
// Vérifier l'expiration du jeton
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."
]);
}
// Vérifier le rôle requis (ex: 'admin')
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;
}
/**
* 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.*,
(SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km
FROM cars c
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' => $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 Authentification opérationnelle',
'database' => DB_NAME,
'comptes_demo' => [
'admin' => ['username' => 'admin', 'password' => 'adminpassword', 'token_fixe' => 'admin-token-secret-12345'],
'user' => ['username' => 'user', 'password' => 'userpassword', 'token_fixe' => 'user-token-secret-67890']
],
'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 le profil de l\'utilisateur connecté',
'POST /auth/logout' => '[AUTHENTIFIÉ] Révoquer le jeton de session'
],
'Voitures' => [
'GET /cars' => '[PUBLIC] Liste des voitures (filtrable par ?immatriculation=...)',
'GET /cars/{id}' => '[PUBLIC] Détails d\'une voiture',
'GET /cars/immatriculation/{plaque}' => '[PUBLIC] Identifier une voiture par sa plaque',
'GET /cars/{id}/etat' => '[AUTHENTIFIÉ] État complet (voiture, dernier km, entretiens, notes)',
'POST /cars' => '[AUTHENTIFIÉ] Créer une voiture (marque, modele, annee, dateAchat, VIN, immatriculation)',
'PUT /cars/{id}' => '[AUTHENTIFIÉ] Modifier les paramètres d\'une voiture',
'DELETE /cars/{id}' => '[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' => '[AUTHENTIFIÉ] Ajouter un relevé kilométrique (valeur, date_releve)',
'DELETE /kilometrage/{id}' => '[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' => '[AUTHENTIFIÉ] Ajouter un entretien (type_entretien, date_evenement, kilometrage, ...)',
'PUT /maintenance/{id}' => '[AUTHENTIFIÉ] Modifier un entretien',
'DELETE /maintenance/{id}' => '[ADMIN] Supprimer un entretien'
],
'Notes (Contenu interne)' => [
'GET /cars/{id}/notes' => '[AUTHENTIFIÉ] Consulter les notes d\'une voiture',
'POST /cars/{id}/notes' => '[AUTHENTIFIÉ] Ajouter une note (titre, contenu)',
'PUT /notes/{id}' => '[AUTHENTIFIÉ] Modifier une note',
'DELETE /notes/{id}' => '[AUTHENTIFIÉ] 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)']);
}
// Génération d'un nouveau jeton valide 7 jours
$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);
// Vérifier existence
$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 (Profil de l'utilisateur connecté)
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 (Révocation du jeton)
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}
// =========================================================================
if ($resource === 'immatriculation' && $method === 'GET') {
$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/immatriculation/{plaque} (Public)
if (isset($segments[1]) && strtolower($segments[1]) === 'immatriculation' && $method === 'GET') {
$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 [CONTENU RESTREINT - AUTHENTIFICATION REQUISE]
if ($carId && $subResource === 'etat' && $method === 'GET') {
$currentUser = authenticate($db, true);
try {
$stmt = $db->prepare('CALL sp_getEtatVoiture(:id)');
$stmt->execute([':id' => $carId]);
$car = $stmt->fetch();
if (!$car) {
sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]);
}
$maintenances = [];
if ($stmt->nextRowset()) {
$maintenances = $stmt->fetchAll();
}
$notes = [];
if ($stmt->nextRowset()) {
$notes = $stmt->fetchAll();
}
$stmt->closeCursor();
sendJson(200, [
'success' => true,
'consulted_by' => $currentUser['username'],
'data' => [
'voiture' => $car,
'maintenances' => $maintenances,
'notes' => $notes
]
]);
} catch (Exception $e) {
$stmtCar = $db->prepare('
SELECT c.*,
(SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km
FROM cars c WHERE c.id = ?
');
$stmtCar->execute([$carId]);
$car = $stmtCar->fetch();
if (!$car) {
sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]);
}
$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' => $currentUser['username'],
'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: [ACTION PROTÉGÉE - AUTHENTIFICATION REQUISE]
if ($method === 'POST') {
authenticate($db, true);
$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: [ACTION PROTÉGÉE - AUTHENTIFICATION REQUISE]
if ($method === 'POST') {
authenticate($db, true);
$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 [CONTENU RESTREINT - AUTHENTIFICATION REQUISE]
if ($carId && $subResource === 'notes') {
// GET: Protégé (Notes de suivi confidentielles)
if ($method === 'GET') {
authenticate($db, true);
$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: [ACTION PROTÉGÉE - AUTHENTIFICATION REQUISE]
if ($method === 'POST') {
authenticate($db, true);
$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 (Public)
if ($carId === null && $subResource === null && $method === 'GET') {
if (!empty($_GET['immatriculation'])) {
findCarByPlate($db, (string)$_GET['immatriculation']);
}
$sql = 'SELECT c.*,
(SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km
FROM cars c
ORDER BY c.id ASC';
$stmt = $db->query($sql);
sendJson(200, ['success' => true, 'data' => $stmt->fetchAll()]);
}
// GET /cars/{id} : Consulter une voiture (Public)
if ($carId !== null && $subResource === null && $method === 'GET') {
$stmt = $db->prepare('
SELECT c.*,
(SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km
FROM cars c
WHERE c.id = ?
');
$stmt->execute([$carId]);
$car = $stmt->fetch();
if (!$car) {
sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]);
}
sendJson(200, ['success' => true, 'data' => $car]);
}
// POST /cars : Créer une voiture [ACTION PROTÉGÉE - AUTHENTIFICATION REQUISE]
if ($carId === null && $method === 'POST') {
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;
// Vérifier unicité de l'immatriculation
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."
]);
}
}
// Appel de la procédure stockée sp_insertVoiture
try {
$stmt = $db->prepare('CALL sp_insertVoiture(:marque, :modele, :annee, :dateAchat, :vin, :immatriculation)');
$stmt->execute([
':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 (marque, modele, annee, dateAchat, VIN, immatriculation) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$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']]);
}
$stmtGet = $db->prepare('
SELECT c.*,
(SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km
FROM cars c
WHERE c.id = ?
');
$stmtGet->execute([$newId]);
sendJson(201, [
'success' => true,
'message' => 'Voiture créée avec succès',
'data' => $stmtGet->fetch()
]);
}
// PUT / PATCH /cars/{id} : Mettre à jour les paramètres [ACTION PROTÉGÉE - AUTHENTIFICATION REQUISE]
if ($carId !== null && $subResource === null && ($method === 'PUT' || $method === 'PATCH')) {
authenticate($db, true);
$check = $db->prepare('SELECT id FROM cars WHERE id = ?');
$check->execute([$carId]);
if (!$check->fetch()) {
sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]);
}
$body = getBody();
$allowedFields = ['marque', 'modele', 'annee', 'dateAchat', 'VIN', 'immatriculation'];
$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);
$stmtGet = $db->prepare('
SELECT c.*,
(SELECT valeur FROM kilometrage WHERE car_id = c.id ORDER BY date_releve DESC, id DESC LIMIT 1) AS dernier_km
FROM cars c
WHERE c.id = ?
');
$stmtGet->execute([$carId]);
sendJson(200, [
'success' => true,
'message' => 'Paramètres de la voiture mis à jour avec succès',
'data' => $stmtGet->fetch()
]);
}
// DELETE /cars/{id} : Supprimer une voiture [ACTION PROTÉGÉE - RÔLE ADMIN REQUIS]
if ($carId !== null && $subResource === null && $method === 'DELETE') {
authenticate($db, true, 'admin');
$check = $db->prepare('SELECT id FROM cars WHERE id = ?');
$check->execute([$carId]);
if (!$check->fetch()) {
sendJson(404, ['success' => false, 'error' => "Voiture #{$carId} introuvable"]);
}
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;
// DELETE: [ACTION PROTÉGÉE - RÔLE ADMIN REQUIS]
if ($kmId && $method === 'DELETE') {
authenticate($db, true, 'admin');
$stmt = $db->prepare('DELETE FROM kilometrage WHERE id = ?');
$stmt->execute([$kmId]);
if ($stmt->rowCount() === 0) {
sendJson(404, ['success' => false, 'error' => "Relevé kilométrique #{$kmId} introuvable"]);
}
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']);
}
// GET: Public
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]);
}
// PUT/PATCH: [ACTION PROTÉGÉE - AUTHENTIFICATION REQUISE]
if ($method === 'PUT' || $method === 'PATCH') {
authenticate($db, true);
$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()
]);
}
// DELETE: [ACTION PROTÉGÉE - RÔLE ADMIN REQUIS]
if ($method === 'DELETE') {
authenticate($db, true, 'admin');
$stmt = $db->prepare('DELETE FROM maintenance_logs WHERE id = ?');
$stmt->execute([$maintId]);
if ($stmt->rowCount() === 0) {
sendJson(404, ['success' => false, 'error' => "Entretien #{$maintId} introuvable"]);
}
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}) [CONTENU RESTREINT - AUTHENTIFICATION REQUISE]
// =========================================================================
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']);
}
// GET: Authentifié
if ($method === 'GET') {
authenticate($db, true);
$stmt = $db->prepare('SELECT * FROM notes WHERE id = ?');
$stmt->execute([$noteId]);
$item = $stmt->fetch();
if (!$item) {
sendJson(404, ['success' => false, 'error' => "Note #{$noteId} introuvable"]);
}
sendJson(200, ['success' => true, 'data' => $item]);
}
// PUT/PATCH: Authentifié
if ($method === 'PUT' || $method === 'PATCH') {
authenticate($db, true);
$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: Authentifié
if ($method === 'DELETE') {
authenticate($db, true);
$stmt = $db->prepare('DELETE FROM notes WHERE id = ?');
$stmt->execute([$noteId]);
if ($stmt->rowCount() === 0) {
sendJson(404, ['success' => false, 'error' => "Note #{$noteId} introuvable"]);
}
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()]);
}