Files
proxy_tmdb/server.js

93 lines
3.0 KiB
JavaScript

import { join } from 'node:path';
import formbody from '@fastify/formbody';
import rateLimit from '@fastify/rate-limit';
import secureSession from '@fastify/secure-session';
import fastifyStatic from '@fastify/static';
import Fastify from 'fastify';
import { HOST, PORT, RATE_LIMIT_PER_SEC, ROOT, SESSION_SECRET } from './config.js';
import { getRatings } from './lib/imdbRatings.js';
import { httpDuration, httpRequests, imdbRatingsCount, searchWorkers } from './lib/metrics.js';
import { getPool } from './lib/searchEngine.js';
import adminRoutes from './routes/admin.js';
import apiRoutes from './routes/api.js';
import healthRoutes from './routes/health.js';
import searchRoutes from './routes/search.js';
const fastify = Fastify({ logger: true, trustProxy: true });
await fastify.register(rateLimit, {
max: RATE_LIMIT_PER_SEC,
timeWindow: '1 second',
// Skip rate limiting for /health and /metrics so monitoring is never throttled
skipOnError: true,
allowList: (req) => req.url === '/health' || req.url === '/metrics',
});
await fastify.register(formbody);
await fastify.register(secureSession, {
secret: SESSION_SECRET.padEnd(32, '0').slice(0, 32),
salt: 'proxytmdb-salt-1',
cookieName: 'session',
cookie: { path: '/', httpOnly: true, sameSite: 'lax', secure: 'auto' },
});
// Per-request timing for Prometheus.
fastify.addHook('onRequest', (_req, reply, done) => {
reply.startTime = process.hrtime.bigint();
done();
});
fastify.addHook('onResponse', (req, reply, done) => {
const route = req.routeOptions?.url || req.url.split('?')[0] || 'unknown';
const labels = { method: req.method, route, status: String(reply.statusCode) };
httpRequests.inc(labels);
if (reply.startTime) {
const seconds = Number(process.hrtime.bigint() - reply.startTime) / 1e9;
httpDuration.observe(labels, seconds);
}
done();
});
// Public static UI at /
await fastify.register(fastifyStatic, {
root: join(ROOT, 'public'),
serve: true,
index: 'index.html',
prefix: '/',
});
await fastify.register(adminRoutes);
await fastify.register(apiRoutes);
await fastify.register(searchRoutes);
await fastify.register(healthRoutes);
// Serve raw project files only under /admin/files (still session-protected
// at the listing level because the listing page itself requires auth).
await fastify.register(fastifyStatic, {
root: ROOT,
serve: true,
index: false,
list: false,
decorateReply: false,
prefix: '/admin/files/',
});
fastify.ready().then(async () => {
try {
const ratings = await getRatings();
imdbRatingsCount.set(ratings.size);
const movie = getPool('movie');
const tv = getPool('tv');
searchWorkers.set({ type: 'movie' }, movie.workers.length);
searchWorkers.set({ type: 'tv' }, tv.workers.length);
fastify.log.info({ ratings: ratings.size }, 'Warmup complete');
} catch (err) {
fastify.log.warn({ err }, 'Warmup failed');
}
});
fastify.listen({ port: PORT, host: HOST }).catch((err) => {
fastify.log.error(err);
process.exit(1);
});