- src/auth.js: OIDC mode (KC_CLIENT_ID/KC_CLIENT_SECRET), session-based middleware, legacy jwt-cookie migration, httpGet/httpPost без внешних зав. - src/routes/auth.js: GET /callback (OIDC), GET+POST /dev-login (backdoor), session-based login/logout для обоих режимов - server.js: express-session (8h, httpOnly, sameSite lax), trust proxy в prod - views/dev-login.ejs: форма с пресетами MOCK_USERS + произвольные поля - views/login.ejs: ссылка на /dev-login если devLoginEnabled - tests: +5 тестов (session middleware), 50 PASS / 0 FAIL Env-переменные: KC_CLIENT_ID, KC_CLIENT_SECRET, KC_BASE_URL, APP_URL — OIDC режим SESSION_SECRET — секрет сессии DEV_MODE=true — /dev-login без пароля DEV_SECRET=xyz — /dev-login с ?key=xyz
151 lines
6.9 KiB
JavaScript
151 lines
6.9 KiB
JavaScript
/**
|
|
* server.js — точка входа приложения.
|
|
*
|
|
* Отвечает только за:
|
|
* 1. Инициализацию Express и глобальных middleware (helmet, static, body-parser)
|
|
* 2. Инициализацию auth (initAuth) и CSRF (initCsrf)
|
|
* 3. Подключение роутеров из src/routes/ с передачей зависимостей
|
|
* 4. Старт HTTP-сервера
|
|
*
|
|
* Бизнес-логика — в src/routes/*.js
|
|
* DB-запросы — в src/queries.js
|
|
* Auth/JWT — в src/auth.js
|
|
* Валидация — в src/validators.js
|
|
* Конфиг/хелперы — в src/config.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const express = require('express');
|
|
const session = require('express-session');
|
|
const cookieParser = require('cookie-parser');
|
|
const path = require('path');
|
|
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 } = require('./src/config');
|
|
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
|
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'));
|
|
|
|
// ── Безопасность HTTP-заголовков ───────────────────────────────────────────────
|
|
// helmet выставляет: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security,
|
|
// Referrer-Policy и пр. CSP отключён явно — EJS использует inline-стили/скрипты.
|
|
app.use(helmet({ contentSecurityPolicy: false }));
|
|
|
|
// За 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();
|
|
|
|
// ── Сессии ────────────────────────────────────────────────────────────────
|
|
// SESSION_SECRET из .env (обязателен в production).
|
|
// MemoryStore — только для одного инстанса; для multi-pod нужен Redis.
|
|
app.use(session({
|
|
secret: process.env.SESSION_SECRET || 'dev-session-secret-change-me',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production', // только HTTPS в проде
|
|
sameSite: 'lax',
|
|
maxAge: 8 * 60 * 60 * 1000, // 8 часов
|
|
},
|
|
}));
|
|
|
|
// ── CSRF защита ────────────────────────────────────────────────────────────
|
|
// initCsrf() читает CSRF_SECRET и NODE_ENV из process.env.
|
|
// Возвращает: doubleCsrfProtection (middleware на POST) и
|
|
// generateCsrfToken (вызов в GET-роутах для передачи токена в шаблон).
|
|
const { doubleCsrfProtection, generateCsrfToken } = initCsrf();
|
|
|
|
// ── Публичные маршруты (до auth.middleware) ──────────────────────────────────
|
|
|
|
// 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);
|
|
|
|
// GET /export: агрегированный список CIDR, без авторизации.
|
|
// src/routes/export.js содержит логику и комментарии.
|
|
app.use(require('./src/routes/export').createRouter({
|
|
q,
|
|
exportLimiter,
|
|
aggregateCIDRs,
|
|
}));
|
|
|
|
// GET /login, POST /login, GET /logout.
|
|
// src/routes/auth.js содержит логику и комментарии.
|
|
app.use(require('./src/routes/auth').createRouter({
|
|
auth,
|
|
doubleCsrfProtection,
|
|
generateCsrfToken,
|
|
MOCK_USERS,
|
|
}));
|
|
|
|
// ── Все маршруты ниже требуют валидного JWT (проверяет auth.middleware) ───────
|
|
app.use(auth.middleware);
|
|
|
|
// 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 при невалидном токене.
|
|
// Ловим ДО общего обработчика — возвращаем понятное сообщение пользователю.
|
|
// 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('Внутренняя ошибка сервера');
|
|
});
|
|
|
|
// ── Старт сервера ─────────────────────────────────────────────────────────────
|
|
checkConnection()
|
|
.then(() => console.log('DB connected'))
|
|
.catch(e => console.error('DB not ready:', e.message));
|
|
|
|
app.listen(PORT, () => console.log('Server on port ' + PORT));
|
|
}
|
|
|
|
start().catch(e => { console.error('Startup error:', e); process.exit(1); });
|
|
|