152 lines
5.8 KiB
JavaScript
152 lines
5.8 KiB
JavaScript
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'); // Disabilitato a favore di CSP
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
// Rimuovi header che rivelano info sul server
|
|
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));
|
|
|
|
// ─── 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('[AUTH] Errore non gestito:', err);
|
|
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);
|
|
});
|