diff --git a/api.php b/api.php index 0482dc4..19e9b4e 100644 --- a/api.php +++ b/api.php @@ -1,470 +1,156 @@ - item.type === currentAdminTab); + document.getElementById('admin-count-label').textContent = `${filtered.length} élément(s)`; -$host = 'localhost'; -$db = 'mon_cinema'; -$user = 'root'; -$pass = ''; -$charset = 'utf8mb4'; -$dsn = "mysql:host=$host;dbname=$db;charset=$charset"; - -try { - $pdo = new PDO($dsn, $user, $pass, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" - ]); -} catch (\PDOException $e) { - echo json_encode(["error" => "Erreur BDD : " . $e->getMessage()]); - exit; + filtered.forEach(f => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + + ${f.poster ? `` : '-'} + ${f.title} + ${f.year || ''} + ${f.director || ''} + ${currentAdminTab === 'critique' ? (f.rating ? '★'.repeat(f.rating) : '☆') : (f.format || '-')} + + + + `; + tbody.appendChild(tr); + }); } -// ── Chiffrement ────────────────────────────────────────────────── -function encryptData($data) { - $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc')); - $encrypted = openssl_encrypt($data, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); - return base64_encode($encrypted . '::' . $iv); +// ── ACTIONS CRUD ── +async function saveFilmForm(e) { + e.preventDefault(); + const payload = { + type: currentAdminTab, + id: document.getElementById('f-id').value, + title: document.getElementById('f-title').value, + year: document.getElementById('f-year').value, + director: document.getElementById('f-director').value, + poster: document.getElementById('f-poster').value, + // Champs conditionnels + rating: document.getElementById('f-rating').value, + review: document.getElementById('f-review').value, + streaming: document.getElementById('f-streaming').value, + format: document.getElementById('f-format').value, + length: document.getElementById('f-length').value, + publisher: document.getElementById('f-publisher').value, + ean_isbn13: document.getElementById('f-ean').value, + number_of_discs: document.getElementById('f-discs').value, + aspect_ratio: document.getElementById('f-aspect').value, + description: document.getElementById('f-description').value + }; + + await fetch(`${API_URL}?action=save_film`, { + method: 'POST', + headers: { 'Authorization': localStorage.getItem('token'), 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + closeAdminModal(); + loadDashboardData(); } -function decryptData($data) { - if (!$data) return ''; - list($encrypted_data, $iv) = explode('::', base64_decode($data), 2); - return openssl_decrypt($encrypted_data, 'aes-256-cbc', ENCRYPTION_KEY, 0, $iv); +async function deleteSingleFilm(id) { + if (!confirm('Supprimer cette œuvre ?')) return; + await fetch(`${API_URL}?action=delete_film&id=${id}&type=${currentAdminTab}`, { + method: 'DELETE', + headers: { 'Authorization': localStorage.getItem('token') } + }); + loadDashboardData(); } -// ── Génération d'ID stable et compatible bigint (PHP 32 et 64 bits) ── -// Produit un entier sur 9 chiffres (< 2^31) → safe partout -// crc32() PHP retourne un int signé 32 bits → on force positif avec & 0x7FFFFFFF -// puis on module pour rester dans une plage confortable sans collision sur ~50k films -function makeStableId($title, $year) { - $key = strtolower(trim($title)) . '|' . trim($year); - $hash = abs(crc32($key)); // abs() = toujours positif - // Reste dans [100000000, 2099999999] → 9-10 chiffres, bien < BIGINT MAX - return ($hash % 2000000000) + 100000000; +// ── MODALES & UI ── +function switchAdminTab(tabName) { + currentAdminTab = tabName; + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.getElementById(`btn-tab-${tabName}`).classList.add('active'); + + // Basculer l'affichage des formulaires + document.getElementById('form-critique-fields').style.display = tabName === 'critique' ? 'block' : 'none'; + document.getElementById('form-videotheque-fields').style.display = tabName === 'videotheque' ? 'block' : 'none'; + + renderAdminTable(); } -// Pour les ajouts manuels : timestamp Unix en secondes (10 chiffres, safe) -function makeNewId() { - // On ajoute un suffixe aléatoire sur 3 chiffres pour éviter les doublons - // si deux films sont ajoutés dans la même seconde - return intval(time() . rand(100, 999)); - // time() = 10 chiffres, rand = 3 → 13 chiffres total - // Mais BIGINT MAX = 9223372036854775807 (19 chiffres) → OK même en 64-bit - // Et en PHP 32-bit, time() seul (10 chiffres) dépasse INT_MAX... - // Solution : utiliser directement crc32 d'un uuid +function openAddModal() { + document.getElementById('film-form').reset(); + document.getElementById('f-id').value = ''; + document.getElementById('admin-modal').classList.add('open'); } -function makeNewIdSafe() { - // Génère un ID unique sur 9-10 chiffres safe en 32 ET 64 bits - $unique = uniqid('', true) . rand(1000, 9999); - return (abs(crc32($unique)) % 2000000000) + 100000000; +function openEditModal(id) { + const item = allItems.find(x => String(x.id) === String(id)); + if (!item) return; + // Remplissage formulaire... + document.getElementById('f-id').value = item.id; + document.getElementById('f-title').value = item.title; + document.getElementById('admin-modal').classList.add('open'); } -// ── Conversion note Letterboxd (/5 avec demi-points) → entier 1-5 ── -function convertRating($rawRating) { - $raw = floatval($rawRating); - if ($raw <= 0) return 3; // pas de note → neutre - // Arrondi au plus proche, clampé entre 1 et 5 - $rounded = (int) round($raw); - return max(1, min(5, $rounded)); +function closeAdminModal() { document.getElementById('admin-modal').classList.remove('open'); } +function openConfigModal() { document.getElementById('config-modal').classList.add('open'); } +function closeConfigModal() { document.getElementById('config-modal').classList.remove('open'); } +function openPasswordModal() { document.getElementById('password-modal').classList.add('open'); } +function closePasswordModal() { document.getElementById('password-modal').classList.remove('open'); } + +function logout() { + localStorage.removeItem('token'); + window.location.href = 'login.html'; } -// ── Authentification ───────────────────────────────────────────── -function checkAuth($pdo) { - $stmtCheck = $pdo->query("SELECT COUNT(*) as total FROM users"); - if ($stmtCheck->fetch()['total'] == 0) return true; - - $token = ''; - if (isset($_SERVER['HTTP_AUTHORIZATION'])) { - $token = $_SERVER['HTTP_AUTHORIZATION']; - } elseif (function_exists('apache_request_headers')) { - $headers = apache_request_headers(); - if (isset($headers['Authorization'])) $token = $headers['Authorization']; - } - - $expectedToken = md5(ENCRYPTION_KEY . 'session'); - if ($token !== $expectedToken) { - http_response_code(403); - echo json_encode(["error" => "Accès interdit. Session expirée."]); - exit; - } +// ── MOT DE PASSE ── +async function saveNewPassword() { + const newPwd = document.getElementById('new-password-input').value; + const confirmPwd = document.getElementById('new-password-confirm').value; + if (newPwd !== confirmPwd) return alert("Les mots de passe ne correspondent pas."); + + const res = await fetch(`${API_URL}?action=update_password`, { + method: 'POST', + headers: { 'Authorization': localStorage.getItem('token'), 'Content-Type': 'application/json' }, + body: JSON.stringify({ new_password: newPwd }) + }); + if (res.ok) { alert("Mot de passe mis à jour."); closePasswordModal(); } } -// ── Requête TMDB ───────────────────────────────────────────────── -function fetchTmdb($url) { - if (function_exists('curl_init')) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, 8); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_USERAGENT, 'MonCinemaApp/1.0'); - $response = curl_exec($ch); - curl_close($ch); - return $response; - } - $opts = [ - "http" => ["method" => "GET", "header" => "User-Agent: MonCinemaApp/1.0\r\n", "timeout" => 8], - "ssl" => ["verify_peer" => false, "verify_peer_name" => false] - ]; - return @file_get_contents($url, false, stream_context_create($opts)); -} - -// ════════════════════════════════════════════════════════════════ -$method = $_SERVER['REQUEST_METHOD']; -$action = $_GET['action'] ?? ''; - -switch ($method) { - - // ════════════════════════════════════════════════════════════ - case 'GET': - - if ($action === 'get_films') { - $stmtC = $pdo->query("SELECT *, 'critique' AS type FROM critiques ORDER BY created_at DESC"); - $critiques = $stmtC->fetchAll(); - $stmtV = $pdo->query("SELECT *, 'videotheque' AS type FROM videotheque ORDER BY created_at DESC"); - $videotheque = $stmtV->fetchAll(); - echo json_encode(array_merge($critiques, $videotheque), JSON_UNESCAPED_UNICODE); - } - - elseif ($action === 'get_tmdb_key_status') { - checkAuth($pdo); - $stmt = $pdo->prepare( - "SELECT COUNT(*) as total FROM config - WHERE key_name = 'tmdb_api_key' - AND key_value IS NOT NULL AND key_value != ''" - ); - $stmt->execute(); - echo json_encode(["exists" => ($stmt->fetch()['total'] > 0)]); - } - - elseif ($action === 'search_tmdb') { - checkAuth($pdo); - $query = $_GET['query'] ?? ''; - $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); - $stmt->execute(); - $res = $stmt->fetch(); - $key = $res ? decryptData($res['key_value']) : ''; - if (!$key) { echo json_encode(["error" => "Clé TMDB non configurée."]); exit; } - $url = "https://api.themoviedb.org/3/search/movie?api_key={$key}&query=" . urlencode($query) . "&language=fr-FR"; - $response = fetchTmdb($url); - echo $response ?: json_encode(["results" => []]); - } - - elseif ($action === 'check_security_status') { - $stmt = $pdo->query("SELECT COUNT(*) as total FROM users"); - echo json_encode(["is_blank" => ($stmt->fetch()['total'] == 0)]); - } - - break; - - // ════════════════════════════════════════════════════════════ - case 'POST': - - // ── Import CSV Letterboxd ───────────────────────────── - if ($action === 'import_csv') { - checkAuth($pdo); - - if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) { - http_response_code(400); - echo json_encode(["error" => "Fichier CSV invalide ou manquant."]); - exit; - } - - // Récupérer la clé TMDB (optionnelle) - $stmtK = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); - $stmtK->execute(); - $resK = $stmtK->fetch(); - $tmdbKey = $resK ? decryptData($resK['key_value']) : ''; - - $file = $_FILES['csv_file']['tmp_name']; - $handle = fopen($file, 'r'); - if (!$handle) { - http_response_code(500); - echo json_encode(["error" => "Impossible d'ouvrir le fichier."]); - exit; - } - - // Gestion BOM UTF-8 (présent ou non) - $bom = fread($handle, 3); - if ($bom !== "\xEF\xBB\xBF") rewind($handle); - - // Lire l'en-tête - $header = fgetcsv($handle, 0, ',', '"'); - if (!$header) { - fclose($handle); - echo json_encode(["error" => "Fichier CSV vide."]); - exit; - } - - // Nettoyer les noms de colonnes (espaces, BOM résiduels) - $header = array_map(function($col) { - return trim(preg_replace('/[\x00-\x1F\x7F\xEF\xBB\xBF]/u', '', $col)); - }, $header); - - $colName = array_search('Name', $header); - $colYear = array_search('Year', $header); - $colRating = array_search('Rating', $header); - $colReview = array_search('Review', $header); // absent dans ratings.csv → false - - if ($colName === false || $colYear === false || $colRating === false) { - fclose($handle); - echo json_encode([ - "error" => "Format non reconnu. Colonnes attendues : Name, Year, Rating. Colonnes trouvées : " . implode(', ', $header) - ]); - exit; - } - - $importedCount = 0; - $errors = []; - - while (($row = fgetcsv($handle, 0, ',', '"')) !== false) { - // Ignorer les lignes trop courtes - if (count($row) <= max($colName, $colYear, $colRating)) continue; - - $title = trim($row[$colName]); - $year = trim($row[$colYear]); - $rating = convertRating($row[$colRating]); - $review = ($colReview !== false && isset($row[$colReview])) ? trim($row[$colReview]) : ''; - - if (empty($title)) continue; - - // ID stable basé sur titre+année, safe en 32 ET 64 bits - $id = makeStableId($title, $year); - - // Récupérer données existantes pour ne pas écraser - $stmtCheck = $pdo->prepare("SELECT review, poster, director FROM critiques WHERE id = :id"); - $stmtCheck->execute([':id' => $id]); - $existing = $stmtCheck->fetch(); - - $poster = $existing['poster'] ?? ''; - $director = $existing['director'] ?? ''; - - // Conserver la critique existante si le CSV n'en fournit pas - if (empty($review) && !empty($existing['review'])) { - $review = $existing['review']; - } - - // Enrichissement TMDB si données manquantes et clé disponible - if ((empty($poster) || empty($director)) && !empty($tmdbKey)) { - $apiUrl = "https://api.themoviedb.org/3/search/movie" - . "?api_key={$tmdbKey}" - . "&query=" . urlencode($title) - . ($year ? "&year={$year}" : '') - . "&language=fr-FR"; - $tmdbRes = fetchTmdb($apiUrl); - - if ($tmdbRes) { - $tmdbData = json_decode($tmdbRes, true); - if (!empty($tmdbData['results'][0])) { - $movieData = $tmdbData['results'][0]; - if (empty($poster) && !empty($movieData['poster_path'])) { - $poster = "https://image.tmdb.org/t/p/w300" . $movieData['poster_path']; - } - if (empty($director) && !empty($movieData['id'])) { - $creditsRes = fetchTmdb("https://api.themoviedb.org/3/movie/{$movieData['id']}/credits?api_key={$tmdbKey}"); - if ($creditsRes) { - $creditsData = json_decode($creditsRes, true); - foreach ($creditsData['crew'] ?? [] as $member) { - if ($member['job'] === 'Director') { - $director = $member['name']; - break; - } - } - } - } - } - } - } - - try { - $stmt = $pdo->prepare( - "REPLACE INTO critiques (id, title, year, director, rating, review, poster, streaming) - VALUES (:id, :title, :year, :director, :rating, :review, :poster, '')" - ); - $stmt->execute([ - ':id' => $id, - ':title' => $title, - ':year' => $year, - ':director' => $director, - ':rating' => $rating, - ':review' => $review, - ':poster' => $poster, - ]); - $importedCount++; - } catch (\PDOException $e) { - $errors[] = "'{$title}' : " . $e->getMessage(); - } - } - - fclose($handle); - - $result = ["success" => true, "imported" => $importedCount]; - if (!empty($errors)) $result['warnings'] = array_slice($errors, 0, 10); - echo json_encode($result, JSON_UNESCAPED_UNICODE); - exit; - } - - // ── Lire le body JSON ───────────────────────────────── - $data = json_decode(file_get_contents('php://input'), true) ?? []; - - // ── Connexion ───────────────────────────────────────── - if ($action === 'login') { - $stmtCheck = $pdo->query("SELECT COUNT(*) as total FROM users"); - if ($stmtCheck->fetch()['total'] == 0) { - echo json_encode([ - "success" => true, - "token" => md5(ENCRYPTION_KEY . 'session'), - "blank" => true - ]); - exit; - } - $stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = 'admin'"); - $stmt->execute(); - $userRow = $stmt->fetch(); - - if ($userRow && password_verify($data['password'] ?? '', $userRow['password_hash'])) { - echo json_encode([ - "success" => true, - "token" => md5(ENCRYPTION_KEY . 'session'), - "blank" => false - ]); - } else { - http_response_code(401); - echo json_encode(["error" => "Mot de passe incorrect."]); - } - } - - // ── Sauvegarde film ─────────────────────────────────── - elseif ($action === 'save_film') { - checkAuth($pdo); - $type = $data['type'] ?? 'critique'; - $id = (!empty($data['id'])) ? intval($data['id']) : makeNewIdSafe(); - - if ($type === 'critique') { - $stmt = $pdo->prepare( - "REPLACE INTO critiques (id, title, year, director, rating, review, poster, streaming) - VALUES (:id, :title, :year, :director, :rating, :review, :poster, :streaming)" - ); - $stmt->execute([ - ':id' => $id, - ':title' => $data['title'] ?? '', - ':year' => $data['year'] ?? '', - ':director' => $data['director'] ?? '', - ':rating' => max(1, min(5, intval($data['rating'] ?? 3))), - ':review' => $data['review'] ?? '', - ':poster' => $data['poster'] ?? '', - ':streaming' => $data['streaming'] ?? '', - ]); - } else { - $stmt = $pdo->prepare( - "REPLACE INTO videotheque - (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description) - VALUES (:id, :title, :year, :director, :poster, :format, :length, :publisher, :ean_isbn13, :number_of_discs, :aspect_ratio, :description)" - ); - $stmt->execute([ - ':id' => $id, - ':title' => $data['title'] ?? '', - ':year' => $data['year'] ?? '', - ':director' => $data['director'] ?? '', - ':poster' => $data['poster'] ?? '', - ':format' => $data['format'] ?? '', - ':length' => !empty($data['length']) ? intval($data['length']) : null, - ':publisher' => $data['publisher'] ?? '', - ':ean_isbn13' => $data['ean_isbn13'] ?? '', - ':number_of_discs' => !empty($data['number_of_discs']) ? intval($data['number_of_discs']) : 1, - ':aspect_ratio' => $data['aspect_ratio'] ?? '', - ':description' => $data['description'] ?? '', - ]); - } - echo json_encode(["success" => true]); - } - - // ── Sauvegarde clé TMDB ─────────────────────────────── - elseif ($action === 'save_tmdb_key') { - checkAuth($pdo); - $stmt = $pdo->prepare( - "REPLACE INTO config (key_name, key_value) VALUES ('tmdb_api_key', :val)" - ); - $stmt->execute([':val' => encryptData($data['tmdb_key'] ?? '')]); - echo json_encode(["success" => true]); - } - - // ── Init compte admin ───────────────────────────────── - elseif ($action === 'setup_admin') { - $pwd = $data['password'] ?? ''; - if (strlen($pwd) < 4) { - http_response_code(400); - echo json_encode(["error" => "Mot de passe trop court (min. 4 caractères)."]); - exit; - } - $stmt = $pdo->prepare( - "REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)" - ); - $stmt->execute([':pass' => password_hash($pwd, PASSWORD_BCRYPT)]); - echo json_encode(["success" => "Compte admin initialisé."]); - } - - // ── Changement mot de passe ─────────────────────────── - elseif ($action === 'update_password') { - checkAuth($pdo); - $newPwd = $data['new_password'] ?? ''; - if (strlen($newPwd) < 4) { - http_response_code(400); - echo json_encode(["error" => "Mot de passe trop court (min. 4 caractères)."]); - exit; - } - $stmt = $pdo->prepare( - "REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)" - ); - $stmt->execute([':pass' => password_hash($newPwd, PASSWORD_BCRYPT)]); - echo json_encode(["success" => "Mot de passe mis à jour."]); - } - - break; - - // ════════════════════════════════════════════════════════════ - case 'DELETE': - - if ($action === 'delete_film') { - checkAuth($pdo); - $id = $_GET['id'] ?? 0; - $type = $_GET['type'] ?? 'critique'; - $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; - $stmt = $pdo->prepare("DELETE FROM {$table} WHERE id = :id"); - $stmt->execute([':id' => $id]); - echo json_encode(["success" => true]); - } - - elseif ($action === 'delete_multiple_films') { - checkAuth($pdo); - $input = json_decode(file_get_contents('php://input'), true) ?? []; - $ids = $input['ids'] ?? []; - $type = $_GET['type'] ?? 'critique'; - $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; - - if (empty($ids) || !is_array($ids)) { - http_response_code(400); - echo json_encode(["error" => "Aucun identifiant fourni."]); - exit; - } - - $ids = array_map('intval', $ids); - $placeholders = implode(',', array_fill(0, count($ids), '?')); - $stmt = $pdo->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})"); - $stmt->execute($ids); - echo json_encode(["success" => true, "deleted_count" => $stmt->rowCount()]); - } - - break; +// ── IMPORT CSV ── +async function handleCsvUpload(input) { + if (!input.files[0]) return; + const formData = new FormData(); + formData.append('csv_file', input.files[0]); + await fetch(`${API_URL}?action=import_csv`, { + method: 'POST', + headers: { 'Authorization': localStorage.getItem('token') }, + body: formData + }); + loadDashboardData(); } \ No newline at end of file