const express = require('express'); const nunjucks = require('nunjucks'); const path = require('path'); const parser = require('cookie-parser'); const database = require('./storage/database'); const app = express(); const PORT = process.env.PORT || 3006; const version = process.env.VERSION; const vBuild = process.env.VERSION_BUILD; const vState = process.env.VERSION_STATE; // ─── SICUREZZA GLOBALE ────────────────────────────────────────────── // Trust proxy (necessario dietro Nginx/Traefik per avere il vero IP del client) app.set('trust proxy', 1); // Rate Limiter in-memory (protezione DoS base, senza dipendenze esterne) const rateLimitStore = new Map(); const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minuto const RATE_LIMIT_MAX = 100; // max richieste per finestra const RATE_LIMIT_AUTH_MAX = 10; // max tentativi login/register per finestra // Pulizia periodica dello store setInterval(() => { const now = Date.now(); for (const [key, entry] of rateLimitStore) { if (now - entry.start > RATE_LIMIT_WINDOW_MS) { rateLimitStore.delete(key); } } }, RATE_LIMIT_WINDOW_MS); function createRateLimiter(maxRequests) { return (req, res, next) => { const key = `${req.ip}:${maxRequests}`; const now = Date.now(); const entry = rateLimitStore.get(key); if (!entry || now - entry.start > RATE_LIMIT_WINDOW_MS) { rateLimitStore.set(key, { count: 1, start: now }); return next(); } entry.count++; if (entry.count > maxRequests) { res.set('Retry-After', Math.ceil((RATE_LIMIT_WINDOW_MS - (now - entry.start)) / 1000)); return res.status(429).json({ error: 'Troppe richieste, riprova più tardi' }); } next(); }; } const globalRateLimit = createRateLimiter(RATE_LIMIT_MAX); const authRateLimit = createRateLimiter(RATE_LIMIT_AUTH_MAX); // Security headers (equivalente leggero di Helmet, senza dipendenze) app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '0'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); res.setHeader( 'Content-Security-Policy', "default-src 'self'; style-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" ); res.removeHeader('X-Powered-By'); next(); }); // Rate limit globale app.use(globalRateLimit); // Limiti dimensione body per prevenire payload eccessivi app.use(express.json({ limit: '16kb' })); app.use(express.urlencoded({ extended: true, limit: '16kb' })); app.use(parser()); // ─── STATIC FILES ─────────────────────────────────────────────────── const staticFolder = path.join(__dirname, 'static'); app.use('/static', express.static(staticFolder)); app.use('/api/static', express.static(staticFolder)); // ─── NUNJUCKS TEMPLATES ───────────────────────────────────────────── const templatesFolder = path.join(__dirname, 'templates'); nunjucks.configure(templatesFolder, { autoescape: true, express: app, noCache: true, watch: false }); app.set('views', templatesFolder); app.set('view engine', 'html'); // ─── ROUTES ───────────────────────────────────────────────────────── // Views (pagine HTML) app.use('/', require('./routes/views/auth')); app.use('/sessions', require('./routes/views/sessions')); app.use('/user', require('./routes/views/user')); // API - Auth (con rate limit più stretto su login/register) const authRoutes = require('./routes/auth'); app.use('/api/auth/login', authRateLimit); app.use('/api/auth/register', authRateLimit); app.use('/api/auth', authRoutes); // API - Risorse app.use('/api/users', require('./routes/users')); app.use('/api/sessions', require('./routes/sessions')); // ─── HEALTH CHECK ─────────────────────────────────────────────────── app.get('/health', async (req, res) => { const dbConnected = await database.checkPostgres(); const redisHelper = require('./storage/redis'); const redisConnected = await redisHelper.checkRedis(); res.json({ status: dbConnected && redisConnected ? "ok" : "degraded", service: "auth", database: dbConnected ? "connected" : "disconnected", redis: redisConnected ? "connected" : "disconnected", version: version, build_number: vBuild, version_state: vState }); }); // ─── 404 HANDLER ──────────────────────────────────────────────────── app.use((req, res) => { res.status(404).json({ error: 'Risorsa non trovata' }); }); // ─── ERROR HANDLER GLOBALE ────────────────────────────────────────── app.use((err, req, res, _next) => { console.error('[ERROR]', err.message, '| code:', err.code); res.status(500).json({ error: 'Errore interno del server' }); }); // ─── STARTUP ──────────────────────────────────────────────────────── async function start() { await database.initDb(); app.listen(PORT, '0.0.0.0', () => { console.log(`[AUTH] Started on port ${PORT}`); }); } start().catch(err => { console.error('[AUTH] Failed to start:', err); process.exit(1); });