// ═══════════════════════════════════════════════════════════════════════════════ // 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 { enhanceImpersonation } = require('./src/impersonation'); const { createUserRouter } = require('./src/user'); const { createAdminRouter } = require('./src/admin'); const { pool } = require('./src/db'); const { ensureSchema } = require('./src/db/schema'); function createV2Router() { // Автосоздание таблиц 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', (req, res) => { const u = req.session && req.session.user; if (!u) return res.send('

Нет данных

Войти

'); res.send(debugPage(u, config.version)); }); // ── GET /v2/iam_response — сырой ответ IAM ───────────────────────── router.get('/iam_response', async (req, res) => { const token = req.session && req.session.token; if (!token) return res.send('

Нет токена

Войти

'); 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_response_test — IAM test стенд ────────────────────── router.get('/iam_response_test', async (req, res) => { const token = req.session && req.session.token; if (!token) return res.send('

Нет токена

Войти

'); try { const raw = await iamFetch(token, 'https://auth-api-test.ngcloud.ru/api/v1/auth/user'); res.set('Content-Type', 'text/html; charset=utf-8'); res.send(rawPage('IAM TEST', raw)); } catch (e) { res.send('

Ошибка IAM Test

' + e.message + '
'); } }); // ── GET /v2/iam_response_dev — IAM dev стенд ──────────────────────── router.get('/iam_response_dev', async (req, res) => { const token = req.session && req.session.token; if (!token) return res.send('

Нет токена

Войти

'); try { const raw = await iamFetch(token, 'https://auth-api-dev.ngcloud.ru/api/v1/auth/user'); res.set('Content-Type', 'text/html; charset=utf-8'); res.send(rawPage('IAM DEV', raw)); } catch (e) { res.send('

Ошибка IAM Dev

' + e.message + '
'); } }); // ── GET /v2/impersonation-check — сравнение auth-api vs lk-api-gateway ── router.get('/impersonation-check', async (req, res) => { const token = req.session && req.session.token; if (!token) return res.send('

Нет токена

Войти

'); const tests = [ { name: 'auth-api /auth/user', url: 'https://auth-api.ngcloud.ru/api/v1/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: 'https://auth-api.ngcloud.ru/api/v1/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', enhanceImpersonation, resolveContext, createUserRouter()); // ── /v2/admin — админка: лимиты + аудит ────────────────────────── router.use('/admin', enhanceImpersonation, resolveContext, createAdminRouter()); // ── GET /v2/ — редирект на /v2/app ────────────────────────────────── router.get('/', (req, res) => res.redirect('/v2/app')); // ── /v2/test — тестовый API (отключить: ENABLE_TEST_API=false) ────── // Код остаётся, роутер не монтируется при ENABLE_TEST_API=false. if (process.env.ENABLE_TEST_API !== 'false') { const { createTestRouter } = require('./src/test'); router.use('/test', createTestRouter()); } // ── /v2/export — выгрузка CIDR (публичный, без авторизации) ────── const { createExportRouter } = require('./src/export'); router.use('/export', createExportRouter()); // ── GET /v2/logout — редирект на основной выход ───────────────────── router.get('/logout', (req, res) => res.redirect('/logout')); return router; } // ── Хелпер: HTTP-запрос к IAM ──────────────────────────────────────────────── async function iamFetch(token, url) { const https = require('https'); const u = new URL(url); return new Promise((resolve, reject) => { 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); } }); }).on('error', reject); }); } 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 };