Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome

This commit is contained in:
unfr
2026-04-24 07:35:10 +02:00
parent f9745a2390
commit a184a21f57
36 changed files with 2060 additions and 364 deletions

View File

@@ -1,54 +1,84 @@
import Fastify from 'fastify';
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 { join } from 'node:path';
import { ROOT, PORT, HOST, SESSION_SECRET } from './config.js';
import indexRoutes from './routes/index.js';
import apiRoutes from './routes/api.js';
import searchRoutes from './routes/search.js';
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';
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, {
// 32 bytes minimum. Use SESSION_SECRET env var in production.
secret: SESSION_SECRET.padEnd(32, '0').slice(0, 32),
salt: 'proxytmdb-salt-1',
cookieName: 'session',
cookie: {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: 'auto',
},
cookie: { path: '/', httpOnly: true, sameSite: 'lax', secure: 'auto' },
});
await fastify.register(indexRoutes);
await fastify.register(apiRoutes);
await fastify.register(searchRoutes);
// 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();
});
// Serve any other path as a static file from the project root, so that the
// "directory listing" links keep working exactly as they did under Apache.
// 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(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: '/',
prefix: '/admin/files/',
});
// Warm up: load IMDb ratings and spawn search workers eagerly.
fastify.ready().then(async () => {
try {
await getRatings();
getPool('movie');
getPool('tv');
fastify.log.info('Warmup complete');
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');
}