Files
watchgether-api/index.php
T
Whykioh 42a388cbe1 Harden for public launch: rate limiting, CORS, error display, Gitea webhook sig
- User::login() locks an account for 15 min after 5 failed attempts
- CORS now restricted to an explicit origin whitelist instead of *
- display_errors disabled in production (errors still logged server-side)
- webhook.php now checks Gitea's actual signature header (X-Gitea-Signature,
  raw hex) instead of GitHub's format, which never matched on this Gitea instance

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 17:53:28 +02:00

168 lines
5.7 KiB
PHP

<?php
// index.php
// En prod on ne balance plus les erreurs PHP au visiteur (fuite d'infos internes) ;
// elles restent loguées côté serveur via error_log.
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
ini_set('log_errors', 1);
error_reporting(E_ALL);
// --- HEADERS CORS ---
// Les apps mobiles natives ne sont pas concernées par CORS (c'est une restriction de navigateur),
// donc restreindre ici n'affecte que les navigateurs web.
$allowed_origins = [
'https://watchgether.whykioh.fr',
'null', // ouverture du HTML en double-clic (file://) pendant le dev local
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_origins, true)) {
header("Access-Control-Allow-Origin: $origin");
}
header("Content-Type: application/json; charset=UTF-8");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
// Si le navigateur fait une pré-requête OPTIONS, on répond OK direct
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit();
}
// --- CONFIGURATION ---
require_once __DIR__ . '/config/database.php';
require_once __DIR__ . '/controllers/UserController.php';
require_once __DIR__ . '/utils/JWT.php';
require_once __DIR__ . '/controllers/MovieController.php';
require_once __DIR__ . '/controllers/TmdbController.php';
// Initialisation de la BDD et du contrôleur
$database = new Database();
$db = $database->getConnection();
$jwt_secret = $database->jwt_secret;
$userController = new UserController($db, $jwt_secret);
$movieController = new MovieController($db);
$tmdbController = new TmdbController($database->tmdb_api_key);
// On récupère la route épurée (ex: si on tape /register, $route vaudra 'register')
$request_uri = explode('?', $_SERVER['REQUEST_URI'], 2)[0];
$route = trim($request_uri, '/');
$method = $_SERVER['REQUEST_METHOD'];
// Récupération des données JSON reçues (ex: les credentials d'inscription)
$data = json_decode(file_get_contents('php://input'), true) ?? [];
// --- FONCTION DE SÉCURISATION DES ROUTES ---
function getAuthenticatedUserId($secret) {
$headers = apache_request_headers();
// On gère les différentes casses possibles pour le header Authorization
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? null;
if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
http_response_code(401);
echo json_encode(["error" => "Accès refusé. Token manquant."]);
exit();
}
$token = $matches[1];
$decoded = JWT::validate($token, $secret);
if (!$decoded) {
http_response_code(401);
echo json_encode(["error" => "Session expirée ou token invalide."]);
exit();
}
return $decoded['user_id']; // Retourne le vrai user_id extrait du token
}
// --- ROUTEUR ---
switch ($route) {
case 'register':
if ($method === 'POST') $userController->register($data);
else http_response_code(405);
break;
case 'login':
if ($method === 'POST') $userController->login($data);
else http_response_code(405);
break;
// --- TOUTES LES ROUTES CI-DESSOUS DEVIENNENT SÉCURISÉES ---
case 'link-partner':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret); // Plus besoin d'envoyer user_id en clair, le token le donne !
$userController->link($data);
} else { http_response_code(405); }
break;
case 'pairing-status':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$userController->pairingStatus($data);
} else { http_response_code(405); }
break;
case 'unlink-partner':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$userController->unlink($data);
} else { http_response_code(405); }
break;
case 'add-movie':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$movieController->add($data);
} else { http_response_code(405); }
break;
case 'share-movie':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$movieController->share($data);
} else { http_response_code(405); }
break;
case 'get-lists':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$movieController->getLists($data);
} else { http_response_code(405); }
break;
case 'remove-movie':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$movieController->remove($data);
} else { http_response_code(405); }
break;
case 'mark-watched':
if ($method === 'POST') {
$data['user_id'] = getAuthenticatedUserId($jwt_secret);
$movieController->markWatched($data);
} else { http_response_code(405); }
break;
case 'tmdb-search':
if ($method === 'POST') {
getAuthenticatedUserId($jwt_secret); // On exige juste un token valide, pas d'usage anonyme du quota TMDB
$tmdbController->search($data);
} else { http_response_code(405); }
break;
case 'tmdb-details':
if ($method === 'POST') {
getAuthenticatedUserId($jwt_secret);
$tmdbController->details($data);
} else { http_response_code(405); }
break;
default:
http_response_code(404);
echo json_encode(["error" => "Route /" . $route . " non trouvée"]);
break;
}