/** * server.js — точка входа приложения. * * Отвечает только за: * 1. Инициализацию Express и глобальных middleware (helmet, static, body-parser) * 2. Инициализацию auth (initAuth) * 3. Подключение роутеров: src/api/routes/ (API), ui/routes/ (UI) * 4. Старт HTTP-сервера * * API-логика — в src/api/routes/ * UI-логика — в ui/routes/ * 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, ensureSchema } = require('./src/db'); const { initAuth } = require('./src/auth'); const q = require('./src/queries'); const { MOCK_USERS, APP_VERSION } = require('./src/config'); const { authLimiter } = require('./src/middleware/rateLimit'); const { createSessionMiddleware } = require('./src/middleware/session'); const { aggregateCIDRs } = require('./src/validators'); const { CSP_DIRECTIVES } = require('./src/middleware/csp'); const { createUiRouter } = require('./ui/index'); 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(); // ── Сессии ──────────────────────────────────────────────────────────────── const { pool } = require('./src/db'); app.use(createSessionMiddleware(pool)); // ── Fail-fast в production ──────────────────────────────────────────────── // Если секреты не заданы — приложение в проде немедленно падает. // Дефолтные значения — только для локальной разработки. if (process.env.NODE_ENV === 'production') { const DEFAULT_SESSION = 'dev-session-secret-change-me'; if (!process.env.SESSION_SECRET || process.env.SESSION_SECRET === DEFAULT_SESSION) { console.error('FATAL: SESSION_SECRET не задан или равен дефолту в production!'); process.exit(1); } if (!process.env.DB_PASS) { console.error('FATAL: DB_PASS не задан в 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); // ── /export — публичный (ВРЕМЕННО без авторизации) ─────────────────────── // 🔮 КОГДА Keycloak SSO: заменить на app.use('/export', auth.bearerMiddleware, ...) app.get('/export', /* 🔮 auth.bearerMiddleware, */ async (req, res) => { try { const cidrs = await q.getExportCIDRs(); const aggregated = aggregateCIDRs(cidrs); const text = aggregated.join('\n') + (aggregated.length ? '\n' : ''); res.set('Content-Type', 'text/plain; charset=utf-8'); const fname = (req.query.filename || 'white-list.txt').replace(/[^\w\-_. ]/g, '_'); const disp = req.query.view === '1' ? 'inline' : 'attachment'; res.set('Content-Disposition', disp + '; filename="' + fname + '"'); res.send(text); } catch (e) { console.error('[export] Error:', e); res.status(500).send('Ошибка экспорта'); } } ); // ── OIDC-роуты (Keycloak) — активны ТОЛЬКО когда KC_CLIENT_ID задан ───── // В mock-режиме (сейчас): заглушки, работает ui/routes/auth.js // 🔮 В проде: задать KC_CLIENT_ID + KC_CLIENT_SECRET в .env const { createRouter: createOidcRouter } = require('./src/routes/oidc'); app.use('/', createOidcRouter({ auth })); // ── REST API v1 (Bearer JWT, без сессий/CSRF) ───────────────────────────── const { createApiRouter } = require('./src/api/index'); app.use('/api/v1', createApiRouter({ auth, q })); // ── UI-слой: SSR через EJS, данные из /api/v1/* ─────────────────────────── // Монтируется ПОСЛЕ /api/v1/ — не перехватывает API-запросы. // Хранит Bearer token в сессии, рендерит те же views/*.ejs. app.use('/', createUiRouter({ auth, MOCK_USERS, authLimiter })); // ── Общий обработчик ошибок ─────────────────────────────────────────────── // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { const ctx = { url: req.method + ' ' + req.originalUrl, user: (req.user && req.user.email) || 'none', clientId: (req.user && req.user.clientId) || '-', }; const detail = err instanceof Error ? (err.stack || err.message || String(err)) : JSON.stringify(err); console.error('Unhandled error:', ctx, detail); res.status(500).send('Ошибка [' + ctx.url + '] user=' + ctx.user + ' clientId=' + ctx.clientId + ' — ' + detail.slice(0, 500)); }); // ── Старт сервера ───────────────────────────────────────────────────────────── checkConnection() .then(() => { console.log('DB connected'); return ensureSchema().catch(e => console.error('[db] Schema error:', e.message)); }) .catch(e => console.error('DB not ready:', e.message)); return app; } // Запускаем сервер только если файл запущен напрямую (не через require) if (require.main === module) { start() .then(a => a.listen(PORT, () => console.log('Server on port ' + PORT))) .catch(e => { console.error('Startup error:', e); process.exit(1); }); } module.exports = { start };