// ═══════════════════════════════════════════════════════════════════════════════ // 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('

Ошибка IAM

' + e.message + '
'); } }); // ── 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 сохранена.

' + 'Войти снова' + ''); }); return; } res.set('Content-Type', 'text/html; charset=utf-8'); res.send('Выход' + '' + '

Выход из сервиса

' + '

Вы будете выведены только из whitelist.
Сессия в других сервисах Nubes сохранится.

' + 'Выйти' + 'Отмена' + ''); }); return router; } // ── Хелпер: HTTP-запрос к IAM ──────────────────────────────────────────────── async function iamFetch(token, url) { const https = require('https'); const u = new URL(url); return new Promise((resolve, reject) => { const req = https.get({ hostname: u.hostname, port: 443, path: u.pathname, headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }, }, res => { let data = ''; res.on('data', c => data += c); res.on('end', () => { if (res.statusCode !== 200) return reject(new Error(res.statusCode + ': ' + data.slice(0, 200))); try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on('error', reject); req.setTimeout(5000, () => { req.destroy(); reject(new Error('timeout')); }); }); } function rawPage(title, data) { return '\n' + title + '' + '' + '

Сырой ответ ' + title + '

' + '
' + JSON.stringify(data, null, 2).replace(/'
    + '

← Сессия | CRUD

' + ''; } 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)}

Выйти | CRUD | Админка

`; } 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(/'; if (r.ok) { html += '
'; html += fmt(r.data, 0); html += '
'; } else { html += '
❌ ' + r.error.replace(/'; } html += '
'; } html += ''; return html; } module.exports = { createV2Router };