refactor: server.js API-only, drop UI routes/session/csrf/ejs
This commit is contained in:
@@ -1,162 +1,57 @@
|
||||
/**
|
||||
* server.js — точка входа приложения.
|
||||
* server.js — API-only точка входа (ветка api-v1).
|
||||
*
|
||||
* Отвечает только за:
|
||||
* 1. Инициализацию Express и глобальных middleware (helmet, static, body-parser)
|
||||
* 2. Инициализацию auth (initAuth) и CSRF (initCsrf)
|
||||
* 3. Подключение роутеров из src/routes/ с передачей зависимостей
|
||||
* 4. Старт HTTP-сервера
|
||||
* Обслуживает только /api/v1/* (Bearer JWT, JSON).
|
||||
* UI — отдельный слой, работает через API.
|
||||
*
|
||||
* Бизнес-логика — в src/routes/*.js
|
||||
* DB-запросы — в src/queries.js
|
||||
* Auth/JWT — в src/auth.js
|
||||
* Валидация — в src/validators.js
|
||||
* Конфиг/хелперы — в src/config.js
|
||||
* Зависимости:
|
||||
* src/auth.js — initAuth (валидация Bearer)
|
||||
* src/queries.js — все SQL-запросы
|
||||
* src/api/ — REST API router
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
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 { checkConnection } = require('./src/db');
|
||||
const { initAuth } = require('./src/auth');
|
||||
const q = require('./src/queries');
|
||||
const { createApiRouter } = require('./src/api/index');
|
||||
|
||||
const app = express();
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
// Безопасные HTTP-заголовки (без CSP браузерного — API не отдаёт HTML)
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
// Версия доступна во всех шаблонах как <%= 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.
|
||||
// За reverse-proxy (nginx/k8s ingress)
|
||||
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 — простая проверка что сервер жив
|
||||
// k8s liveness probe
|
||||
app.get('/healthz', (req, res) => res.send('OK'));
|
||||
|
||||
// JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме)
|
||||
// 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,
|
||||
}));
|
||||
|
||||
// ── REST API v1 (Bearer JWT, без сессий/CSRF) ─────────────────────────────
|
||||
const { createApiRouter } = require('./src/api/index');
|
||||
// REST API v1
|
||||
app.use('/api/v1', createApiRouter({ auth, q }));
|
||||
|
||||
// ── Все маршруты ниже требуют авторизации (auth.middleware) ────────────────
|
||||
app.use(auth.middleware);
|
||||
// 404 для всего остального
|
||||
app.use((req, res) => res.status(404).json({ error: 'Not found' }));
|
||||
|
||||
// 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 при невалидном токене.
|
||||
// Ловим ДО общего обработчика — возвращаем понятное сообщение пользователю.
|
||||
// Общий error handler
|
||||
// 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(
|
||||
'<p style="font-family:sans-serif;padding:2rem">' +
|
||||
'Недействительный CSRF-токен. ' +
|
||||
'<a href="javascript:history.back()">Вернитесь назад</a> и повторите действие.' +
|
||||
'</p>'
|
||||
);
|
||||
}
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).send('Внутренняя ошибка сервера');
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// ── Старт сервера ─────────────────────────────────────────────────────────────
|
||||
checkConnection()
|
||||
.then(() => console.log('DB connected'))
|
||||
.catch(e => console.error('DB not ready:', e.message));
|
||||
@@ -164,7 +59,6 @@ async function start() {
|
||||
return app;
|
||||
}
|
||||
|
||||
// Запускаем сервер только если файл запущен напрямую (не через require)
|
||||
if (require.main === module) {
|
||||
start()
|
||||
.then(a => a.listen(PORT, () => console.log('Server on port ' + PORT)))
|
||||
@@ -172,4 +66,3 @@ if (require.main === module) {
|
||||
}
|
||||
|
||||
module.exports = { start };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user