diff --git a/server.js b/server.js index 380b1ab..a61ea86 100644 --- a/server.js +++ b/server.js @@ -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( - '
' + - 'Недействительный CSRF-токен. ' + - 'Вернитесь назад и повторите действие.' + - '
' - ); - } 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 }; -