Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
import { join } from 'node:path';
|
2026-04-23 08:37:48 +02:00
|
|
|
import formbody from '@fastify/formbody';
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
import rateLimit from '@fastify/rate-limit';
|
2026-04-23 08:37:48 +02:00
|
|
|
import secureSession from '@fastify/secure-session';
|
|
|
|
|
import fastifyStatic from '@fastify/static';
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
import Fastify from 'fastify';
|
|
|
|
|
import { HOST, PORT, RATE_LIMIT_PER_SEC, ROOT, SESSION_SECRET } from './config.js';
|
2026-04-23 08:37:48 +02:00
|
|
|
import { getRatings } from './lib/imdbRatings.js';
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
import { httpDuration, httpRequests, imdbRatingsCount, searchWorkers } from './lib/metrics.js';
|
2026-04-23 08:37:48 +02:00
|
|
|
import { getPool } from './lib/searchEngine.js';
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
import adminRoutes from './routes/admin.js';
|
|
|
|
|
import apiRoutes from './routes/api.js';
|
|
|
|
|
import healthRoutes from './routes/health.js';
|
2026-04-24 07:37:29 +02:00
|
|
|
import searchRoutes from './routes/search.js';
|
2026-04-23 08:37:48 +02:00
|
|
|
|
|
|
|
|
const fastify = Fastify({ logger: true, trustProxy: true });
|
|
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
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',
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-23 08:37:48 +02:00
|
|
|
await fastify.register(formbody);
|
|
|
|
|
|
|
|
|
|
await fastify.register(secureSession, {
|
|
|
|
|
secret: SESSION_SECRET.padEnd(32, '0').slice(0, 32),
|
|
|
|
|
salt: 'proxytmdb-salt-1',
|
|
|
|
|
cookieName: 'session',
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
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();
|
2026-04-23 08:37:48 +02:00
|
|
|
});
|
|
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
// Public static UI at /
|
|
|
|
|
await fastify.register(fastifyStatic, {
|
|
|
|
|
root: join(ROOT, 'public'),
|
|
|
|
|
serve: true,
|
|
|
|
|
index: 'index.html',
|
|
|
|
|
prefix: '/',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await fastify.register(adminRoutes);
|
2026-04-23 08:37:48 +02:00
|
|
|
await fastify.register(apiRoutes);
|
2026-04-24 07:37:29 +02:00
|
|
|
await fastify.register(searchRoutes);
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
await fastify.register(healthRoutes);
|
2026-04-23 08:37:48 +02:00
|
|
|
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
// Serve raw project files only under /admin/files (still session-protected
|
|
|
|
|
// at the listing level because the listing page itself requires auth).
|
2026-04-23 08:37:48 +02:00
|
|
|
await fastify.register(fastifyStatic, {
|
|
|
|
|
root: ROOT,
|
|
|
|
|
serve: true,
|
|
|
|
|
index: false,
|
|
|
|
|
list: false,
|
|
|
|
|
decorateReply: false,
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
prefix: '/admin/files/',
|
2026-04-23 08:37:48 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fastify.ready().then(async () => {
|
|
|
|
|
try {
|
Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
2026-04-24 07:35:10 +02:00
|
|
|
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');
|
2026-04-23 08:37:48 +02:00
|
|
|
} catch (err) {
|
|
|
|
|
fastify.log.warn({ err }, 'Warmup failed');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fastify.listen({ port: PORT, host: HOST }).catch((err) => {
|
|
|
|
|
fastify.log.error(err);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|