Files
ipwhitelist-app/server.js
T

131 lines
6.7 KiB
JavaScript

/**
* 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 { initCsrf } = require('./src/middleware/csrf');
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);
// ── OIDC-роуты (Keycloak) ───────────────────────────────────────────────
const { createRouter: createOidcRouter } = require('./src/routes/oidc');
app.use('/', createOidcRouter({ auth }));
// ── CSRF — инициализация после сессий ─────────────────────────────────
const { generateCsrfToken, doubleCsrfProtection } = initCsrf();
// ── V2 — основной код ─────────────────────────────────────────────────
app.get('/', (req, res) => res.redirect('/v2/app'));
// Редирект /export → /v2/export (для обратной совместимости)
app.get('/export', (req, res) => res.redirect('/v2/export' + (req.originalUrl.includes('?') ? req.originalUrl.substring(req.originalUrl.indexOf('?')) : '')));
const { createV2Router } = require('./v2/server');
app.use('/v2', createV2Router({ generateCsrfToken, doubleCsrfProtection }));
// ── Общий обработчик ошибок ───────────────────────────────────────────────
// 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 };