163 lines
8.1 KiB
JavaScript
163 lines
8.1 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 (без exit — сообщение в лог) ────────────────────
|
|
const DEFAULT_SESSION = 'dev-session-secret-change-me';
|
|
let fatalError = null;
|
|
if (process.env.NODE_ENV === 'production') {
|
|
if (!process.env.SESSION_SECRET || process.env.SESSION_SECRET === DEFAULT_SESSION)
|
|
fatalError = 'SESSION_SECRET не задан или равен дефолту в production';
|
|
else if (!process.env.DB_PASS)
|
|
fatalError = 'DB_PASS не задан в production';
|
|
else if (!process.env.CSRF_SECRET || process.env.CSRF_SECRET === 'dev-csrf-secret-change-in-prod')
|
|
fatalError = 'CSRF_SECRET не задан или равен дефолту в production';
|
|
}
|
|
|
|
// k8s liveness + readiness probes
|
|
app.get('/healthz', (req, res) => res.send('OK'));
|
|
app.get('/ready', async (req, res) => {
|
|
try {
|
|
await pool.query('SELECT 1');
|
|
res.send('OK');
|
|
} catch (e) {
|
|
res.status(503).send('DB not ready');
|
|
}
|
|
});
|
|
|
|
// Если фатальная ошибка конфигурации — показываем на всех страницах сообщение
|
|
if (fatalError) {
|
|
console.error(fatalError);
|
|
app.use((req, res) => {
|
|
res.status(503).type('html').send(errorPage(fatalError));
|
|
});
|
|
return app;
|
|
}
|
|
|
|
// JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме)
|
|
if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler);
|
|
|
|
// ── OIDC-роуты (Keycloak) ───────────────────────────────────────────────
|
|
app.use('/login', authLimiter); // защита от брутфорса
|
|
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('Внутренняя ошибка сервера');
|
|
});
|
|
|
|
// ── Старт сервера ─────────────────────────────────────────────────────────────
|
|
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));
|
|
// Graceful shutdown по SIGTERM (k8s)
|
|
process.on('SIGTERM', () => {
|
|
console.log('SIGTERM — shutting down...');
|
|
const { pool } = require('./src/db');
|
|
pool.end().then(() => { console.log('DB pool closed'); process.exit(0); });
|
|
setTimeout(() => { console.log('Forced exit'); process.exit(0); }, 10000);
|
|
});
|
|
})
|
|
.catch(e => {
|
|
console.error('Startup error:', e.message);
|
|
// Не падаем — запускаем HTTP с сообщением об ошибке
|
|
const errorApp = require('express')();
|
|
errorApp.use((req, res) => {
|
|
res.status(503).type('html').send('<html><head><meta charset="utf-8"><title>Авария</title></head><body style="font-family:sans-serif;padding:40px;text-align:center"><h1>⚙️ Сервис временно недоступен</h1><p>Причина: ' + (e.message || 'неизвестна') + '</p><p>Попробуйте обновить страницу через минуту</p></body></html>');
|
|
});
|
|
errorApp.listen(PORT, () => console.log('Server on port ' + PORT + ' (ERROR MODE)'));
|
|
});
|
|
}
|
|
|
|
module.exports = { start };
|
|
|