Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions api/feedbackFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ interface VercelResponse extends ServerResponse {
json(body: unknown): void;
}

const MAX_BODY_BYTES = 10_000; // 10 KB - plenty for a feedback message

export default async function handler(
req: IncomingMessage,
res: VercelResponse,
Expand All @@ -20,14 +22,43 @@ export default async function handler(
return;
}

const contentType = req.headers['content-type'] ?? '';
if (!contentType.includes('application/json')) {
res.status(415).json({ error: 'Content-Type must be application/json' });
return;
}

let body: string;
try {
const body = await new Promise<string>((resolve, reject) => {
body = await new Promise<string>((resolve, reject) => {
let data = '';
req.on('data', (chunk) => (data += chunk));
req.on('data', (chunk: Buffer) => {
data += chunk;
if (data.length > MAX_BODY_BYTES) {
reject(new Error('PAYLOAD_TOO_LARGE'));
}
});
req.on('end', () => resolve(data));
req.on('error', reject);
});
} catch (err) {
if (err instanceof Error && err.message === 'PAYLOAD_TOO_LARGE') {
res.status(413).json({ error: 'Payload too large' });
} else {
res.status(500).json({ error: 'Failed to read request body' });
}
return;
}

// Validate that the body is actually parseable JSON before forwarding.
try {
JSON.parse(body);
} catch {
res.status(400).json({ error: 'Invalid JSON body' });
return;
}

try {
const upstream = await fetch(endpoint, {
method: 'POST',
headers: {
Expand Down
12 changes: 12 additions & 0 deletions api/tmdbFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ interface VercelResponse extends ServerResponse {

const TMDB_BASE = 'https://api.themoviedb.org/3';

// Only the two paths the app actually needs — anything else is rejected.
const ALLOWED_PATH_PREFIXES = ['/search/movie', '/movie/'];

function isAllowedPath(path: string): boolean {
return ALLOWED_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
}

export default async function handler(
req: VercelRequest,
res: VercelResponse,
Expand All @@ -27,6 +34,11 @@ export default async function handler(
return;
}

if (!isAllowedPath(path)) {
res.status(403).json({ error: 'Forbidden' });
return;
}

const upstream = new URLSearchParams();
for (const [k, v] of Object.entries(req.query)) {
if (k !== 'path') upstream.set(k, Array.isArray(v) ? v[0] : v);
Expand Down
36 changes: 35 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,41 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/CueMovie_transparent.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CueMovie</title>

<title>CueMovie - Random Movie Picker From Your Watchlist</title>
<meta
name="description"
content="Can't decide what to watch? Upload your IMDb or Letterboxd watchlist and let CueMovie pick a random movie for you. Filter by genre, rating, runtime, and more."
/>

<!-- Open Graph (WhatsApp, Facebook, iMessage, Telegram previews) -->
<meta property="og:title" content="CueMovie - Random Movie Picker" />
<meta
property="og:description"
content="Can't decide what to watch? Upload your IMDb or Letterboxd watchlist and let CueMovie pick a random movie for you."
/>
<meta
property="og:image"
content="https://www.cuemovie.app/CueMovie_transparent.png"
/>
<meta property="og:url" content="https://www.cuemovie.app" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="CueMovie" />

<!-- Twitter/X card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="CueMovie - Random Movie Picker" />
<meta
name="twitter:description"
content="Can't decide what to watch? Upload your IMDb or Letterboxd watchlist and let CueMovie pick a random movie for you."
/>
<meta
name="twitter:image"
content="https://www.cuemovie.app/CueMovie_transparent.png"
/>

<!-- Canonical URL -->
<link rel="canonical" href="https://www.cuemovie.app" />
</head>
<body>
<div id="root"></div>
Expand Down
Loading
Loading