fix: add TMDB proxy endpoint and axios interceptor to hide API key

- Add GET /api/tmdb-proxy/* backend route that proxies TMDB API calls
  using the server-side TMDB_API_KEY (process.env), keeping it hidden
  from clients
- Add frontend axios interceptor (registerTmdbProxyInterceptor) that
  automatically rewrites any request to api.themoviedb.org/3/ through
  the backend proxy, stripping the api_key parameter from the URL
- Register interceptor on both default axios and api instances
This commit is contained in:
Satanas1275 2026-07-02 00:14:59 +02:00
parent 1b41d85795
commit d6e72a7e18
4 changed files with 105 additions and 1 deletions

View file

@ -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
// ---------------------------------------------------------------------------

View file

@ -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';
export const TMDB_IMAGE_URL = 'https://image.tmdb.org/t/p';

View file

@ -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(
<StrictMode>
<ErrorBoundary>

View file

@ -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)
);
}