/** * server.js — точка входа приложения. * * Отвечает только за: * 1. Инициализацию Express и глобальных middleware (helmet, static, body-parser) * 2. Инициализацию auth (initAuth) и CSRF (initCsrf) * 3. Подключение роутеров из src/routes/ с передачей зависимостей * 4. Старт HTTP-сервера * * Бизнес-логика — в src/routes/*.js * DB-запросы — в src/queries.js * Auth/JWT — в src/auth.js * Валидация — в src/validators.js * Конфиг/хелперы — в src/config.js */ 'use strict'; const express = require('express'); const cookieParser = require('cookie-parser'); const path = require('path'); const helmet = require('helmet'); require('dotenv').config(); const { checkConnection } = require('./src/db'); const { initAuth, requireAdmin } = require('./src/auth'); const { aggregateCIDRs } = require('./src/validators'); const q = require('./src/queries'); const { MOCK_USERS, backUrl, APP_VERSION } = require('./src/config'); const { mutationLimiter, exportLimiter, authLimiter } = require('./src/middleware/rateLimit'); const { initCsrf } = require('./src/middleware/csrf'); const { createSessionMiddleware } = require('./src/middleware/session'); const { CSP_DIRECTIVES } = require('./src/middleware/csp'); const app = express(); const PORT = process.env.PORT || 3000; app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // Версия доступна во всех шаблонах как <%= appVersion %> без передачи вручную. app.locals.appVersion = APP_VERSION; // ── Безопасность HTTP-заголовков ─────────────────────────────────────────────── // helmet выставляет все security-заголовки одним вызовом, включая CSP. // Директивы CSP вынесены в src/middleware/csp.js. app.use(helmet({ contentSecurityPolicy: { directives: CSP_DIRECTIVES } })); // За reverse-proxy (nginx/k8s ingress) — доверяем первому хопу для secure cookies. if (process.env.NODE_ENV === 'production') app.set('trust proxy', 1); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.urlencoded({ extended: true, limit: '32kb' })); app.use(cookieParser()); async function start() { const auth = await initAuth(); // ── Сессии ──────────────────────────────────────────────────────────────── // PG-стор: сессии переживают рестарт, работают на нескольких подах. // Таблица `session` создаётся автоматически при первом старте. // src/middleware/session.js содержит логику и комментарии. const { pool } = require('./src/db'); app.use(createSessionMiddleware(pool)); // ── CSRF защита ──────────────────────────────────────────────────────────── // initCsrf() читает CSRF_SECRET и NODE_ENV из process.env. // Возвращает: doubleCsrfProtection (middleware на POST) и // generateCsrfToken (вызов в GET-роутах для передачи токена в шаблон). const { doubleCsrfProtection, generateCsrfToken } = initCsrf(); // ── Публичные маршруты (до auth.middleware) ────────────────────────────────── // ── Fail-fast в production ──────────────────────────────────────────────── // Если секреты не заданы — приложение в проде немедленно падает. // Дефолтные значения — только для локальной разработки. if (process.env.NODE_ENV === 'production') { const DEFAULT_SESSION = 'dev-session-secret-change-me'; const DEFAULT_CSRF = 'dev-csrf-secret-change-in-prod'; if (!process.env.SESSION_SECRET || process.env.SESSION_SECRET === DEFAULT_SESSION) { console.error('FATAL: SESSION_SECRET не задан или равен дефолту в production!'); process.exit(1); } if (!process.env.CSRF_SECRET || process.env.CSRF_SECRET === DEFAULT_CSRF) { console.error('FATAL: CSRF_SECRET не задан или равен дефолту в production!'); process.exit(1); } } // k8s liveness probe — простая проверка что сервер жив app.get('/healthz', (req, res) => res.send('OK')); // JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме) if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler); // GET /login, POST /login, GET /logout. // src/routes/auth.js содержит логику и комментарии. app.use(require('./src/routes/auth').createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS, authLimiter, })); // ── Все маршруты ниже требуют авторизации (auth.middleware) ──────────────── app.use(auth.middleware); // GET /export: CIDR текущего пользователя (или всех компаний для admin). // Перемещён за auth.middleware — изоляция данных по компании. // src/routes/export.js содержит логику и комментарии. app.use(require('./src/routes/export').createRouter({ q, exportLimiter, aggregateCIDRs, })); // GET /, POST /add, POST /edit/:id, POST /delete/:id. // src/routes/entries.js содержит логику и комментарии. app.use(require('./src/routes/entries').createRouter({ q, doubleCsrfProtection, mutationLimiter, generateCsrfToken, backUrl, })); // GET /audit, GET /admin, POST /admin/limit/:companyId. // src/routes/admin.js содержит логику и комментарии. app.use(require('./src/routes/admin').createRouter({ q, requireAdmin, doubleCsrfProtection, mutationLimiter, generateCsrfToken, })); // ── CSRF error handler ──────────────────────────────────────────────────────── // doubleCsrfProtection вызывает next(err) с кодом 403 при невалидном токене. // Ловим ДО общего обработчика — возвращаем понятное сообщение пользователю. // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { if (err && (err.status === 403 || err.code === 'EBADCSRFTOKEN')) { return res.status(403).send( '
' + 'Недействительный CSRF-токен. ' + 'Вернитесь назад и повторите действие.' + '
' ); } console.error('Unhandled error:', err); res.status(500).send('Внутренняя ошибка сервера'); }); // ── Старт сервера ───────────────────────────────────────────────────────────── checkConnection() .then(() => console.log('DB connected')) .catch(e => console.error('DB not ready:', e.message)); app.listen(PORT, () => console.log('Server on port ' + PORT)); } start().catch(e => { console.error('Startup error:', e); process.exit(1); });