// ═══════════════════════════════════════════════════════════════════════════════ // V2 — главный роутер (монтируется в server.js как /v2) // // ЭТО: createV2Router() — собирает все v2-роутеры в один. // ЗАЧЕМ: единственная точка входа v2 в основное приложение. // // МОНТАЖ В ОСНОВНОМ server.js: // const { createV2Router } = require('./v2/server'); // app.use('/v2', createV2Router()); // // ПОРЯДОК МАРШРУТОВ (важен!): // 1. /login — редирект на основной OIDC-вход // 2. /iam — отладка IAM-данных из сессии // 3. /app — CRUD юзера (resolveContext + user роутер) // 4. /admin — админка (resolveContext + admin роутер) // 5. /test — тестовый API (если ENABLE_TEST_API !== 'false') // 6. /logout — редирект на основной выход // // ВАЖНО: роутеры /app и /admin используют resolveContext middleware, // которое проверяет сессию и выставляет req.clientId, req.email, ... // ═══════════════════════════════════════════════════════════════════════════════ 'use strict'; const express = require('express'); const config = require('./src/config'); const auth = require('./src/auth'); const { resolveContext } = require('./src/router'); const { createUserRouter } = require('./src/user'); const { createAdminRouter } = require('./src/admin'); const { pool } = require('./src/db'); const { ensureSchema } = require('./src/db/schema'); const { mutationLimiter, exportLimiter } = require('../src/middleware/rateLimit'); function createV2Router({ generateCsrfToken, doubleCsrfProtection } = {}) { // Автосоздание таблиц v2_companies, v2_entries, v2_audit при старте ensureSchema(pool).catch(e => console.error('[v2:db] Schema error:', e.message)); const router = express.Router(); // ── GET /v2/login — редирект на основной вход (KC OIDC) ───────────── router.get('/login', (req, res) => { res.redirect('/login?returnTo=' + encodeURIComponent('/v2/app')); }); // ── GET /v2/iam — отладка: IAM-данные из сессии (только админ) ──── router.get('/iam', resolveContext, (req, res) => { if (!req.canAdmin) return res.status(403).send('Доступ запрещён'); const u = req.session && req.session.user; res.send(debugPage(u, config.version)); }); // ── GET /v2/iam_response — сырой ответ IAM (только админ) ───────── router.get('/iam_response', resolveContext, async (req, res) => { if (!req.canAdmin) return res.status(403).send('Доступ запрещён'); const token = req.session && req.session.token; if (!token) return res.redirect('/v2/login'); try { const mainAuth = require('../src/auth'); const iamData = await mainAuth.fetchIamUser(token); res.set('Content-Type', 'text/html; charset=utf-8'); res.send(rawPage('IAM PROD', iamData.raw)); } catch (e) { res.send('
' + e.message + ''); } }); // ── GET /v2/db-status — проверка подключения к PG (для отладки) ───── router.get('/db-status', async (req, res) => { try { const { pool } = require('./src/db'); const dns = require('dns').promises; const host = process.env.DB_HOST || 'localhost'; const ips = await dns.resolve4(host).catch(() => ['dns-failed']); const start = Date.now(); await pool.query('SELECT 1 AS ok'); const result = { ok: true, db_host: host, resolved_ips: ips, query_ms: Date.now() - start }; return res.json(result); } catch (e) { return res.json({ ok: false, error: e.message, db_host: process.env.DB_HOST || 'unknown' }); } }); // ── GET /v2/iam-gateway-test — сравнение API (только админ) ──────── router.get('/iam-gateway-test', resolveContext, async (req, res) => { if (!req.canAdmin) return res.status(403).send('Доступ запрещён'); const token = req.session && req.session.token; if (!token) return res.redirect('/v2/login'); const base = config.iamApiBase; const tests = [ { name: 'auth-api /auth/user', url: base + '/auth/user', }, { name: 'lk-api-gateway /iam/auth/user', url: 'https://lk-api-gateway.ngcloud.ru/api/v1/iam/auth/user', }, { name: 'auth-api /impersonation/status', url: base + '/impersonation/status', }, ]; const results = {}; for (const t of tests) { try { const data = await iamFetch(token, t.url); results[t.name] = { ok: true, data }; } catch (e) { results[t.name] = { ok: false, error: e.message }; } } res.set('Content-Type', 'text/html; charset=utf-8'); res.send(iamComparePage(results)); }); // ── /v2/app — пользовательский CRUD (только с сессией) ────────────── router.use('/app', mutationLimiter, resolveContext, doubleCsrfProtection || ((req,res,next)=>next()), createUserRouter({ generateCsrfToken })); // ── /v2/admin — админка: лимиты + аудит ────────────────────────── router.use('/admin', mutationLimiter, resolveContext, doubleCsrfProtection || ((req,res,next)=>next()), createAdminRouter({ generateCsrfToken })); // ── GET /v2/ — редирект на /v2/app ────────────────────────────────── router.get('/', (req, res) => res.redirect('/v2/app')); // ── /v2/test — тестовый API (только для ADMIN_EMAIL) ──────────── if (process.env.ENABLE_TEST_API !== 'false') { const { createTestRouter } = require('./src/test'); router.use('/test', resolveContext, (req, res, next) => { if (req.email !== (process.env.ADMIN_EMAIL || '')) return res.status(403).send('Доступ запрещён'); next(); }, createTestRouter()); } // ── /v2/export — выгрузка CIDR (только для админов) ───────────── const { createExportRouter } = require('./src/export'); router.use('/export', exportLimiter, resolveContext, (req, res, next) => { if (!req.canAdmin) return res.status(403).send('Доступ запрещён — только для администраторов'); next(); }, createExportRouter()); // ── GET /v2/logout — выход только из whitelist (без KC SSO) ───────── router.get('/logout', (req, res) => { const ua = (req.headers['user-agent'] || '').toLowerCase(); const isBrowser = ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari'); if (!isBrowser) { req.session.destroy(() => res.redirect('/v2/login')); return; } if (req.query.confirm === 'yes') { req.session.destroy(() => { res.set('Content-Type', 'text/html; charset=utf-8'); res.send('
Сессия whitelist завершена.
Сессия в других сервисах Nubes сохранена.
Вы будете выведены только из whitelist.
Сессия в других сервисах Nubes сохранится.
' + JSON.stringify(data, null, 2).replace(/'
+ ''
+ '';
}
function debugPage(u, version) {
return `
V2 — IAM
IAM Response (v${version})
Email: ${u.email}
clientId: ${u.clientId}
Компания: ${u.companyName}
isAdmin: ${u.isAdmin}
ФИО: ${(u.fio && u.fio.fullName) || u.fio || '—'}
Компаний: ${u.allClientIds ? u.allClientIds.length : 0} — ${(u.allClientIds || []).join(', ')}
${JSON.stringify(u, null, 2)}
`;
}
function iamComparePage(results) {
function fmt(data, depth) {
if (data === null || data === undefined) return 'null';
if (typeof data === 'string') return '"' + data.replace(/';
if (typeof data === 'number' || typeof data === 'boolean') return '' + data + '';
if (Array.isArray(data)) {
if (data.length === 0) return '[]';
let html = '[';
for (const item of data) html += '' + fmt(item, depth + 1) + ',';
html += ']';
return html;
}
if (typeof data === 'object') {
let html = '{';
for (const [k, v] of Object.entries(data)) {
html += '"' + k + '": ' + fmt(v, depth + 1) + '';
}
html += '}';
return html;
}
return String(data).replace(/IAM Impersonation ';
html += '';
html += '🧪 IAM Impersonation — GET эндпоинты
';
html += '';
for (const [name, r] of Object.entries(results)) {
html += '';
html += 'GET /impersonation/' + name + '';
if (r.ok) {
html += '';
html += fmt(r.data, 0);
html += '';
} else {
html += '❌ ' + r.error.replace(/';
}
html += '';
}
html += '';
return html;
}
module.exports = { createV2Router };