diff --git a/API/Mainapi/routes/tmdb.js b/API/Mainapi/routes/tmdb.js index d61bfe9..e4444b8 100644 --- a/API/Mainapi/routes/tmdb.js +++ b/API/Mainapi/routes/tmdb.js @@ -842,6 +842,54 @@ router.get('/SenpaiStream/tv/cache/:tmdbId', async (req, res) => { } }); +// --------------------------------------------------------------------------- +// GET /tmdb-proxy/* -- Generic proxy to TMDB API (hides API key from client) +// --------------------------------------------------------------------------- +router.get('/tmdb-proxy/*', async (req, res) => { + try { + // Extract the TMDB API path from the wildcard + // e.g. /api/tmdb-proxy/genre/movie/list -> genre/movie/list + const tmdbPath = req.params[0] || req.url.replace(/^\/+tmdb-proxy\//, ''); + if (!tmdbPath) { + return res.status(400).json({ error: 'TMDB path is required' }); + } + + // Forward query parameters (except api_key which would be passed by the client) + const { api_key, ...forwardParams } = req.query; + const params = new URLSearchParams(); + params.set('api_key', TMDB_API_KEY); + params.set('language', forwardParams.language || 'fr-FR'); + for (const [key, value] of Object.entries(forwardParams)) { + if (key !== 'language') { + params.set(key, String(value)); + } + } + + const url = `${TMDB_API_URL}/${tmdbPath}?${params.toString()}`; + const response = await axios.get(url, { + timeout: 10000, + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Movix/1.0', + }, + }); + + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Cache-Control', 'public, max-age=300'); // 5min cache + res.json(response.data); + } catch (error) { + if (error.response) { + return res.status(error.response.status).json({ + error: 'TMDB API error', + status: error.response.status, + }); + } + console.error('[TMDB Proxy] Error:', error.message); + res.status(502).json({ error: 'TMDB proxy failed' }); + } +}); + // --------------------------------------------------------------------------- // Exports // --------------------------------------------------------------------------- diff --git a/src/config/config.ts b/src/config/config.ts index 33ff797..b7118f9 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,4 +1,4 @@ // TMDB Configuration export const TMDB_API_KEY = import.meta.env.VITE_TMDB_API_KEY || ''; export const TMDB_API_URL = 'https://api.themoviedb.org/3'; -export const TMDB_IMAGE_URL = 'https://image.tmdb.org/t/p'; \ No newline at end of file +export const TMDB_IMAGE_URL = 'https://image.tmdb.org/t/p'; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 6ce7cc5..8e62d02 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,8 @@ import App from './App.tsx' import axios from 'axios' import { api } from './services/api' import { registerBlockDetection } from './services/blockDetection' +import { registerTmdbProxyInterceptor } from './services/tmdbProxyInterceptor' +import { MAIN_API } from './config/runtime' import './index.css' import './styles/light-mode.css' @@ -126,6 +128,10 @@ startMovixConsoleSafetyWarning(); registerBlockDetection(axios) registerBlockDetection(api) +// Register TMDB proxy interceptor to hide API key from client +registerTmdbProxyInterceptor(axios, MAIN_API) +registerTmdbProxyInterceptor(api, MAIN_API) + createRoot(document.getElementById('root')!).render( diff --git a/src/services/tmdbProxyInterceptor.ts b/src/services/tmdbProxyInterceptor.ts new file mode 100644 index 0000000..f44a712 --- /dev/null +++ b/src/services/tmdbProxyInterceptor.ts @@ -0,0 +1,50 @@ +import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; + +const TMDB_HOST = 'api.themoviedb.org'; +const TMDB_PATH_PREFIX = '/3/'; + +/** + * Intercepteur axios qui redirige les appels TMDB vers le proxy backend + * pour masquer la clé API côté client. + * + * Au lieu d'appeler https://api.themoviedb.org/3/genre/movie/list?api_key=XXX, + * la requête est redirigée vers /api/tmdb-proxy/genre/movie/list?api_key=XXX + * et le backend ajoute sa propre clé TMDB (process.env.TMDB_API_KEY). + * + * La clé API envoyée par le frontend est ignorée par le backend. + */ +export function registerTmdbProxyInterceptor(axiosInstance: AxiosInstance, mainApiUrl: string) { + return axiosInstance.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + if (!config.url || !config.baseURL) return config; + + let fullUrl: string; + try { + fullUrl = new URL(config.url, config.baseURL).toString(); + } catch { + return config; + } + + // Ne toucher qu'aux requêtes vers api.themoviedb.org + const parsed = new URL(fullUrl); + if (parsed.hostname !== TMDB_HOST || !parsed.pathname.startsWith(TMDB_PATH_PREFIX)) { + return config; + } + + // Extraire le chemin TMDB (ex: /3/genre/movie/list -> genre/movie/list) + const tmdbPath = parsed.pathname.replace(TMDB_PATH_PREFIX, ''); + const proxyUrl = `${mainApiUrl}/api/tmdb-proxy/${tmdbPath}`; + + // Reconstruire les paramètres, en supprimant api_key (le backend ajoute la sienne) + const params = new URLSearchParams(parsed.search); + params.delete('api_key'); + + const paramsString = params.toString(); + config.url = paramsString ? `${proxyUrl}?${paramsString}` : proxyUrl; + config.baseURL = ''; // Éviter que axios re-prépende baseURL + + return config; + }, + (error) => Promise.reject(error) + ); +}