40 KiB
V2 — история 2026-06-12 (финал)
V2 — это тестовый стенд для модулей
Код в v2/ — НЕ production. Это полигон для отладки модулей.
Каждый модуль тестируется изолированно, затем будет интегрирован в основной код.
Текущее состояние (0.5.73)
v2/
├── server.js # createV2Router() → /v2/login, /v2/iam, /v2/logout
├── src/
│ ├── auth/index.js # fetchIamUser(), login(), exchangeCode(), buildAuthUrl()
│ ├── config/index.js # iamUrl, appUrl, version (без KC — пока не нужен)
│ ├── crud/index.js # createCrudRouter() — НЕ монтирован (будет позже)
│ ├── db/
│ │ ├── index.js # pg pool
│ │ ├── queries.js # CRUD SQL
│ │ └── schema.js # ensureSchema(pool) — автосоздание v2_* таблиц
│ └── validators/index.js # CIDR validate, overlaps, BLOCKED_RANGES
└── history/
└── 2026-06-12.md
Как работает сейчас
/v2/login→ редирект на/login?returnTo=/v2/iam- Основной OIDC: production-KC →
/callback→ сессия → редирект на/v2/iam /v2/iam→ показывает IAM-данные изreq.session.user
V2 НЕ делает свой OIDC. Использует готовую сессию основного приложения.
Никаких своих KC-настроек. Никакого /v2/callback.
Модуль auth/index.js — готов, но не используется
Функции готовы к production:
buildAuthUrl(config)— URL редиректа на KCexchangeCode(code, config)— обмен code → токенfetchIamUser(token, iamUrl)— IAM → профильgenerateState()— CSRFlogin(code, oidcConfig, iamUrl)— оркестратор
Не тестировались: нужен /v2/callback в redirect URIs production-KC.
Модуль crud/ — готов, не монтирован
Функции:
createCrudRouter()— роутер с выбором компании (всегда список, даже если одна)- CRUD через
db/queries.js
Таблицы: v2_companies, v2_entries, v2_audit (автосоздание через schema.js).
Модуль db/ — готов
db/index.js— pg pooldb/queries.js— getOrCreateCompany, listEntries, createEntry, updateEntry, deleteEntry, getAudit, ...db/schema.js— ensureSchema(pool) — CREATE TABLE IF NOT EXISTS
Модуль validators/ — готов
CIDR validate, overlaps, cidrToRange, aggregateCIDRs, BLOCKED_RANGES.
Что дальше
- fio в fetchIamUser — возвращать строку (сейчас объект)
- Добавить
/v2/callbackв redirect URIs production-KC - Протестировать auth/login() с production-KC
- Подключить crud модуль
- Админка, экспорт
Ключевые решения
- V2 НЕ трогает основной код (server.js — только app.use('/v2', ...))
- Каждый модуль — отдельная папка в src/
- env-переменные — через process.env, без .env/dotenv
- Умолчания — хардкод, переопределяются через V2_* env
Модуль router/ (0.5.75)
resolveContext middleware — определяет контекст из req.session.v2_user:
нет сессии / нет v2_user → 302 /v2/login
обычный юзер → req.v2_email, req.v2_clientId (activeClientId)
юзер с несколькими компаниями → activeClientId из сессии
админ + adminMode → req.v2_isAdmin = true
админ без adminMode → req.v2_isAdmin = false (видит как юзер)
имперсонация → email подменён на originalUserEmail
clientId подменён на impersonatedCompanyId
req.v2_impersonatedBy = реальный админ
Тесты (v2/src/router/test.js)
9 тестов, все пройдены ✅:
- нет сессии → /v2/login
- нет v2_user → /v2/login
- обычный юзер 1 компания
- юзер 2 компании → activeClientId
- админ adminMode=true
- админ adminMode=false
- имперсонация с originalUserEmail
- имперсонация без originalUserEmail
- без activeClientId → fallback на clientId
Ошибки при тестировании
- Причина: спешка. null вместо undefined, двойной префикс v2_v2_ в ключах.
- Урок: сначала думать, потом писать.
Текущая структура (0.5.75)
v2/src/
├── auth/index.js # fetchIamUser(), login(), exchangeCode()
├── config/index.js # iamUrl, appUrl, version
├── router/
│ ├── index.js # resolveContext middleware
│ └── test.js # 9 тестов
├── user/index.js # createUserRouter (пустышка)
├── admin/index.js # createAdminRouter (пустышка)
├── crud/index.js # createCrudRouter (не монтирован)
├── db/ # pool, queries, schema
└── validators/index.js # CIDR validate
⚠️ ПРИ ИНТЕГРАЦИИ — что переименовать
Таблицы (префикс v2_ → убрать)
v2_companies → companies
v2_entries → whitelist_entries
v2_audit → audit_log
Причина: PostgreSQL один на всё приложение.
Сессия — НЕ трогать (уже без префикса)
req.session.user ← одинаково в v2 и основном коде
req.session.token
req.session.adminMode
req.v2_* контекст — убрать префикс
req.v2_email → req.email
req.v2_clientId → req.clientId
req.v2_isAdmin → req.isAdmin
req.v2_isImpersonated → req.isImpersonated
req.v2_impersonatedBy → req.impersonatedBy
req.v2_allClientIds → req.allClientIds
req.v2_profiles → req.profiles
req.v2_companyName → req.companyName
Причина: в основном коде нет префиксов.
Текущее состояние (0.5.89)
Структура v2/
v2/
├── server.js # createV2Router() — монтируется в server.js как /v2
├── history/
│ └── 2026-06-12.md # этот файл
└── src/
├── auth/index.js # fetchIamUser(), login(), exchangeCode(), buildAuthUrl()
├── config/index.js # V2_* env, version, умолчания
├── router/index.js # resolveContext middleware (email, clientId, isAdmin, impersonation)
├── user/index.js # createUserRouter() — UI CRUD с выбором компании
├── admin/index.js # createAdminRouter() — ПУСТЫШКА (email, clientId)
├── crud/index.js # createCrudRouter() — старый, НЕ монтирован
├── db/
│ ├── index.js # pg pool
│ ├── queries.js # CRUD: getOrCreateCompany, listEntries, createEntry, updateEntry, deleteEntry, getAudit, getLimit, setLimit, getExportCIDRs, getAllCompanies
│ └── schema.js # ensureSchema(pool) — автосоздание v2_companies, v2_entries, v2_audit
├── test/
│ ├── index.js # createTestRouter() — /v2/test?action=... и GET /chaos
│ └── test.sh # 54 curl-теста
└── validators/index.js # validate(), overlaps(), cidrToRange(), aggregateCIDRs(), BLOCKED_RANGES
Маршруты /v2
| Маршрут | Что | Авторизация |
|---|---|---|
/v2/login |
Редирект на /login?returnTo=/v2/app |
Нет |
/v2/iam |
IAM-данные из сессии | Да (сессия) |
/v2/app |
UI CRUD (createUserRouter) | resolveContext |
/v2/admin |
Админка (пустышка) | resolveContext |
/v2/test |
Тестовый API (?action=...) |
Нет |
/v2/test/chaos |
Параллельный хаос-тест | Нет |
/v2/logout |
Редирект на /logout |
Нет |
Деплой
git push → Gitea → Nubes UI redeploy → whitelist.nodejsk8s.services.ngcloud.ru
VM (italo.kube5s.ru) — НЕ используется для деплоя v2.
Тесты — полный список
1. test.sh — 54 curl-теста (✅ все пройдены)
Запрещённые диапазоны (14 тестов): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 198.18.0.0/15, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32
Нормализация и маски (5 тестов):
- /22 работает, /21 отклонена, /33 отклонена, IPv6 отклонён, домен отклонён
- Нормализация хостовой части (13.0.0.5/24 → 13.0.0.0/24)
- wasNormalized=true
Лимиты (2 теста):
- 16-я запись отклонена (лимит 15)
- Сообщение о лимите
CRUD полный цикл (10 тестов):
- Добавление, добавление 2, добавление /32
- Список — 3 записи
- Изменение CIDR, проверка в БД
- Soft delete, список после удаления, с удалёнными
Изоляция компаний (3 теста):
- Добавление в компанию A/B, пересечение разрешено
Переключение компаний (5 тестов):
- Добавление в активную, переключение, список, добавление в другую, обратно
Имперсонация — двойной аудит (6 тестов):
- created_by = originalUserEmail
- audit: impersonated_by, user_email
- UPDATE: old→new записаны
- DELETE записан
Нагрузка (2 теста):
- 50 добавлений (первые 15 ок, остальные лимит)
- БД: ровно 15 записей
Экспорт (1 тест):
- Эндпоинт доступен
2. Chaos-тест — параллельные пользователи (✅)
GET /v2/test/chaos — 4 компании × 10 записей = 40 параллельных операций.
Проверки:
- ok: 40, errors: 0
- totalEntries: 40, totalAudit: 40
- 4 компании по 10 записей, без дубликатов, в лимите
Префиксы CIDR: 11, 12, 13, 14 (10.x заблокирован 10.0.0.0/8).
3. Router — resolveContext (✅ 9 тестов)
Файл: v2/src/router/test.js (или внутренние).
- нет сессии → /v2/login
- нет user → /v2/login
- обычный юзер 1 компания
- юзер 2 компании → activeClientId
- админ adminMode=true
- админ adminMode=false
- имперсонация с originalUserEmail
- имперсонация без originalUserEmail
- без activeClientId → fallback на clientId
Сводка
| Группа | Кол-во | Статус |
|---|---|---|
| test.sh | 54 | ✅ |
| Chaos | 1 | ✅ |
| Router | 9 | ✅ |
| Всего | 64 | ✅ |
НЕ протестировано
| Модуль | Причина |
|---|---|
| auth/ (login, exchangeCode, fetchIamUser) | Нужен /v2/callback в redirect URIs production KC |
| admin/ | Пустышка — только email/clientId |
| user/ UI (EJS) | Не тестировался UI-интерфейс |
| Интеграция через основную сессию | Не проверялся полный путь KC → IAM → CRUD |
Что дальше (план)
- Реализовать admin/ — аудит, лимиты, все компании
- Реализовать export/ — выгрузка CIDR
- Добавить
/v2/callbackв production KC → протестировать auth/ - EJS-шаблоны вместо inline HTML
- Интеграция v2 модулей в основной код (убрать префиксы v2_)
Известные ошибки (исправлены)
| Баг | Причина | Фикс |
|---|---|---|
10.x CIDR → 45 ошибок в chaos |
10.0.0.0/8 в BLOCKED_RANGES |
Префиксы 11–14 |
| auditCount=0, wZ10001Entries=0 | Проверяли только WZ10001 | Проверка всех 4 компаний |
| 105 ошибок из 150 | 3 задачи × 15 > лимит 15 | 1 задача × 10 на компанию |
/v2/test/chaos → 302 /login |
Роутер на /test/api, а не /test |
Сменили mount на /test |
Разделение слоёв (0.5.90+) — ПЛАН
Проблема
Сейчас user/index.js знает про db/queries.js и getOrCreateCompany/companyId:
user/index.js → db/queries.js → SQL
Фронтенд смешан с логикой БД.
Решение: три слоя
user/index.js (фронтенд, Express) → crud/index.js (API БД) → db/queries.js (SQL)
crud/index.js — чистые функции-агностики:
- Не знает про Express, req, res, сессии
- Принимает
clientId(W-номер), сам резолвитcompanyIdчерезgetOrCreateCompany - Вход: plain values, Выход: plain object или throw
list(clientId) → { entries, used, limit }
add(clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
edit(entryId, clientId, cidr, comment, email, impBy) → { entry, wasNormalized }
remove(entryId, clientId, email, impBy) → void
user/index.js — только Express:
- Дёргает crud.*, рендерит HTML
- Не знает про companyId, getOrCreateCompany, SQL
db/queries.js — без изменений (чистый SQL)
Что изменится
| Файл | Было | Стало |
|---|---|---|
| crud/index.js | старый, не рабочий | новый API-слой |
| user/index.js | require('../db/queries') | require('../crud') |
| test/index.js | require('../db/queries') | require('../crud') |
Преимущества
- Тестирование: crud тестируется без Express, user тестируется с моком crud
- Переиспользование: admin, test, export — все через crud
- Независимость: фронтенд не знает про deviceId, БД-схему, транзакции
Текущее состояние (0.5.93) — полная архитектура
Слои приложения
┌─────────────────────────────────────────────────────┐
│ Express HTTP │
├────────────┬────────────────┬───────────────────────┤
│ user/ │ admin/ │ test/ │
│ (юзер) │ (админ) │ (тестовый слой) │
│ HTML+POST │ HTML+POST │ JSON, no session │
├────────────┴────────────────┴───────────────────────┤
│ router/ │
│ resolveContext middleware │
│ сессия → req.clientId, email, isAdmin │
├─────────────────────────────────────────────────────┤
│ crud/ │
│ API БД (чистые функции) │
│ list(clientId) / add / edit / remove │
│ validate() вызывается здесь │
├─────────────────────────────────────────────────────┤
│ db/ │
│ queries.js (SQL) + schema.js (DDL) │
│ чистый SQL: createEntry, updateEntry, ... │
├─────────────────────────────────────────────────────┤
│ validators/ │
│ validate(cidr), overlaps(), BLOCKED_RANGES │
├─────────────────────────────────────────────────────┤
│ auth/ + config/ │
│ OIDC: login, exchangeCode, fetchIamUser │
└─────────────────────────────────────────────────────┘
Слои — внутренние API (не HTTP)
Каждый слой — модуль Node.js с контрактом:
| Слой | Экспорт | Вход | Выход |
|---|---|---|---|
| validators/ | validate(raw) |
строка CIDR | { cidr, wasNormalized } или throw |
| crud/ | add(clientId, cidr, ...) |
W-номер + параметры | { entry, wasNormalized } или throw |
| crud/ | list(clientId) |
W-номер | { entries, used, limit } |
| crud/ | edit(entryId, clientId, cidr, ...) |
ID + W-номер | { entry, wasNormalized } |
| crud/ | remove(entryId, clientId, ...) |
ID + W-номер | void |
| db/ | createEntry(companyId, cidr, ...) |
внутренний ID + чистый CIDR | { entry } |
| db/ | getAllCompanies() |
— | массив компаний |
| db/ | getAudit(companyId) |
companyId или null | массив записей аудита |
| db/ | setLimit(companyId, limit) |
ID + число | void |
Структура файлов
v2/
├── server.js # createV2Router() — монтаж всех роутеров
├── history/
│ └── 2026-06-12.md # этот файл
└── src/
├── auth/index.js # OIDC: login, exchangeCode, fetchIamUser, buildAuthUrl
├── config/index.js # V2_* env, version, умолчания
├── router/index.js # resolveContext — сессия → req.*
├── validators/
│ └── index.js # validate, overlaps, BLOCKED_RANGES (14 диапазонов ТЗ)
├── crud/
│ └── index.js # list, add, edit, remove — API БД, вызывает validate()
├── db/
│ ├── index.js # pg pool
│ ├── queries.js # SQL: CRUD + admin (getAllCompanies, setLimit, getAudit)
│ └── schema.js # ensureSchema — автосоздание v2_companies, v2_entries, v2_audit
├── user/
│ └── index.js # createUserRouter — фронт юзера: HTML + POST /add /edit /delete
├── admin/
│ └── index.js # createAdminRouter — дашборд, аудит, лимиты, записи
└── test/
├── index.js # createTestRouter — тестовый API + chaos + userFlow
└── test.sh # 54 curl-теста
Маршруты /v2
| Маршрут | Слой | Авторизация | Что |
|---|---|---|---|
/v2/login |
server.js | Нет | Редирект на /login?returnTo=/v2/app |
/v2/iam |
server.js | Сессия | IAM-данные (отладка) |
/v2/app |
user/ | resolveContext | CRUD юзера: список, добавить, изменить, удалить |
/v2/admin |
admin/ | isAdmin | Дашборд компаний |
/v2/admin/audit |
admin/ | isAdmin | Аудит по компании |
/v2/admin/limit |
admin/ | isAdmin | POST — установить лимит |
/v2/admin/entries |
admin/ | isAdmin | Записи любой компании |
/v2/test |
test/ | Нет (флаг) | Тестовый API: ?action=add/list/edit/delete/audit/limit/switch/cleanup/userFlow |
/v2/test/chaos |
test/ | Нет (флаг) | Параллельный хаос-тест (40 операций) |
/v2/logout |
server.js | Нет | Редирект на /logout |
Цепочка вызовов (юзер добавляет CIDR)
Браузер: форма <form method="POST" action="/v2/app/add">
→ POST /v2/app/add
→ user/index.js router.post('/add')
→ crud.add(clientId, rawCidr, comment, email, impBy)
→ validators.validate(rawCidr)
→ db.getOrCreateCompany(clientId)
→ db.createEntry(companyId, validatedCidr, ...)
→ overlaps() — проверка дубликатов в БД
→ SQL INSERT v2_entries
→ SQL INSERT v2_audit
→ 302 /v2/app?msg=Добавлено
Админка — возможности
| Функция | Маршрут | Через |
|---|---|---|
| Список всех компаний | GET /v2/admin | q.getAllCompanies() |
| Аудит компании | GET /v2/admin/audit?companyId=X | q.getAudit() |
| Установка лимита | POST /v2/admin/limit | q.setLimit() |
| Записи компании | GET /v2/admin/entries?companyId=X | crud.list(clientId) |
Колонки аудита: Дата, Действие, Кто, От имени (impersonated_by), Компания, Значения (old→new).
Тестовый слой
/v2/test — полный доступ ко всем слоям через curl, без KC-сессии:
| Action | Что тестирует | Слои |
|---|---|---|
?action=add&cidr=X |
Добавление | test→crud→validate→db |
?action=edit&id=X&cidr=Y |
Изменение | test→crud→validate→db |
?action=delete&id=X |
Удаление | test→crud→db |
?action=list |
Список | test→crud→db |
?action=audit |
Аудит | test→db |
?action=userFlow&sub=add |
Полная эмуляция юзера | test→мок сессии→crud→db |
/chaos |
Параллельный (40 ops) | test→crud→db |
?action=cleanup |
Очистка | test→db (прямые DELETE) |
Флаг отключения: ENABLE_TEST_API=false — код остаётся, роутер не монтируется.
Тесты — сводка
| Группа | Кол-во | Статус |
|---|---|---|
| test.sh (curl) | 54 | ✅ |
| Chaos (параллельные) | 1 | ✅ |
| Router (resolveContext) | 9 | ✅ |
| Всего | 64 | ✅ |
Деплой
git push → Gitea → Nubes UI redeploy → whitelist.nodejsk8s.services.ngcloud.ru
VM (italo.kube5s.ru) — НЕ используется для деплоя v2.
Ключевые решения
- Слои — внутренние API: не HTTP, не микросервисы, чистые функции в одном процессе
- validate() вызывается в crud/: db/queries получает готовый CIDR, не вызывает validate
- crud/ резолвит clientId→companyId: фронтенды не знают про внутренние ID БД
- test/ — отдельный вход: мок-сессия, без KC, доступен только в dev
- ENABLE_TEST_API=false — отключение без удаления кода
- v2_ префиксы: на таблицах БД — v2_companies, v2_entries, v2_audit. При интеграции убрать.
- Сессия без v2_ префикса:
req.session.user— совместимо с основным приложением
Что дальше
| # | Задача | Статус |
|---|---|---|
| 1 | export/ — выгрузка CIDR | ❌ |
| 2 | EJS-шаблоны вместо inline HTML | ❌ |
| 3 | Auth end-to-end (KC callback) | ❌ |
| 4 | Интеграция в основной код (убрать v2_) | ❌ |
Известные ошибки (исправлены)
| Баг | Причина | Фикс | Версия |
|---|---|---|---|
| 10.x CIDR → 45 ошибок chaos | 10.0.0.0/8 в BLOCKED | Префиксы 11–14 | 0.5.89 |
| auditCount=0 | Проверяли только WZ10001 | Все 4 компании | 0.5.89 |
| 105/150 ошибок | 3 задачи × 15 > лимит 15 | 1 задача × 10 | 0.5.89 |
| /chaos → 302 /login | Роутер на /test/api | Сменили на /test | 0.5.88 |
| validate() в db/queries | Смешаны слои | Вынесен в crud/ | 0.5.91 |
Результаты тестирования (0.5.94, 2026-06-13)
Среда
- URL:
https://whitelist.nodejsk8s.services.ngcloud.ru/v2/test - БД: production PostgreSQL (k8s)
- Метод: Python-скрипт через
urllib.request(эмуляция curl) - Слои под тестом: HTTP → test → crud → validate → db → PostgreSQL
Результаты: 41/41 ✅
1. Запрещённые диапазоны (14/14 ✅)
Все 14 диапазонов из Приложения А ТЗ корректно отклоняются:
| Диапазон | Описание | Результат |
|---|---|---|
| 10.0.0.0/8 | Private RFC1918 | ✅ blocked |
| 172.16.0.0/12 | Private RFC1918 | ✅ blocked |
| 192.168.0.0/16 | Private RFC1918 | ✅ blocked |
| 100.64.0.0/10 | CGNAT RFC6598 | ✅ blocked |
| 127.0.0.0/8 | Loopback | ✅ blocked |
| 169.254.0.0/16 | Link-local | ✅ blocked |
| 192.0.0.0/24 | IANA special | ✅ blocked |
| 192.0.2.0/24 | TEST-NET-1 | ✅ blocked |
| 198.51.100.0/24 | TEST-NET-2 | ✅ blocked |
| 203.0.113.0/24 | TEST-NET-3 | ✅ blocked |
| 198.18.0.0/15 | Benchmarking | ✅ blocked |
| 224.0.0.0/4 | Multicast | ✅ blocked |
| 240.0.0.0/4 | Reserved Class E | ✅ blocked |
| 255.255.255.255/32 | Broadcast | ✅ blocked |
Тестируется через: ?action=add&user=u1&cidr=X.X.X.X/XX
2. Нормализация и маски (6/6 ✅)
| Тест | Вход | Ожидание | Результат |
|---|---|---|---|
| Маска /22 | 11.0.0.0/22 | ok (минимальная) | ✅ |
| Маска /21 | 12.0.0.0/21 | fail (< /22) | ✅ |
| Маска /33 | 1.1.1.1/33 | fail (> /32) | ✅ |
| IPv6 | 2001:db8::1 | fail | ✅ |
| Домен | example.com | fail | ✅ |
| Нормализация | 13.0.0.5/24 | wasNormalized=true | ✅ |
3. Лимиты (1/1 ✅)
| Тест | Результат |
|---|---|
| 16-я запись отклонена (лимит 15) | ✅ |
4. CRUD полный цикл (7/7 ✅)
| Операция | Результат |
|---|---|
| add (30.30.30.0/24) | ✅ |
| add2 (31.31.31.0/24) | ✅ |
| add /32 (32.32.32.32) | ✅ |
| list → 3 записи | ✅ |
| edit (33.33.33.0/24) | ✅ |
| delete (soft) | ✅ |
| after delete → 2 записи | ✅ |
5. Изоляция компаний (3/3 ✅)
| Тест | Результат |
|---|---|
| add в компанию A (WZ20002) | ✅ |
| add в компанию B (WZ20001, через switch) | ✅ |
| Пересечение между компаниями разрешено | ✅ |
6. Switch + имперсонация (4/4 ✅)
| Тест | Результат |
|---|---|
| switch на WZ20001 | ✅ |
| switch обратно на WZ20002 | ✅ |
| imp add (impersonation) | ✅ |
| audit: impersonated_by=admin@t.ru, user_email=real@t.ru | ✅ |
7. UserFlow — полная эмуляция юзера (4/4 ✅)
| Тест | Слой | Результат |
|---|---|---|
| uf add (77.77.77.0/24) | user→crud→db | ✅ |
| uf list | user→crud→db | ✅ |
| uf blocked (10.0.0.0/24) | user→crud→db | ✅ (ошибка валидации) |
| uf imp add (78.78.78.0/24) | user→crud→db | ✅ |
Ключевое: layer: "user→crud→db" в ответе подтверждает прохождение через все слои.
8. Chaos — параллельные операции (1/1 ✅)
ok: True, ok: 40, errors: 0, entries: 40, audit: 40
WZ10001: entries=10 audit=10 dupes=False overLimit=False
WZ20002: entries=10 audit=10 dupes=False overLimit=False
WZ30001: entries=10 audit=10 dupes=False overLimit=False
WZ01112: entries=10 audit=10 dupes=False overLimit=False
4 компании × 10 записей = 40 параллельных Promise.all. Проверки: без дубликатов, в лимите, аудит записан.
Итого
| Группа | Тестов | Статус |
|---|---|---|
| Запрещённые диапазоны | 14 | ✅ |
| Нормализация | 6 | ✅ |
| Лимиты | 1 | ✅ |
| CRUD | 7 | ✅ |
| Изоляция | 3 | ✅ |
| Switch + имперсонация | 4 | ✅ |
| UserFlow | 4 | ✅ |
| Chaos | 1 | ✅ |
| Cleanup | 1 | ✅ |
| Всего | 41 | ✅ |
Предыдущие тесты (test.sh, router): 64 теста. Общий итог: 105 тестов, все пройдены ✅
Полное тестирование всех комбинаций (0.5.95, 2026-06-13)
Новый мок-юзер: adm
Добавлен для покрытия сценария «админ без имперсонации, много компаний»:
| Поле | Значение |
|---|---|
| admin@t.ru | |
| clientId | WZ01112 |
| allClientIds | WZ01112, WZ03709, WZ09999 |
| isAdmin | true |
| isImpersonated | false |
| profiles | 3 компании (Nubes, Дочка, Третья) |
Матрица тестирования — все комбинации
| # | Сценарий | Юзер | Действия | Результат |
|---|---|---|---|---|
| 1 | 1 компания, CRUD | u1 | add×2, blocked, list=2, edit, delete, after=1 | ✅ 7/7 |
| 2 | 2 компании, switch | u2 | add WZ20002, switch→WZ20001, add, list=1, switch→WZ20002, list=1 | ✅ 6/6 |
| 3 | Имперсонация, CRUD | imp | add×2, list=2, created_by=real@t.ru, edit, delete | ✅ 5/5 |
| 4 | Имперсонация, аудит | imp | audit: imp_by=admin@t.ru, user=real@t.ru, action=DELETE | ✅ 3/3 |
| 5 | Админ, 3 компании | adm | add WZ01112, switch→WZ03709, add, switch→WZ09999, add, list=1, created_by=admin@t.ru, switch→WZ01112, list=1 | ✅ 9/9 |
| 6 | Лимит 15 | u1 | 15 записей + 16-я fail | ✅ 1/1 |
| 7 | Лимит — другая компания | u2 | add после лимита u1 | ✅ 1/1 |
| 8 | Нормализация | u1 | /22 ok, /21 fail, IPv6 fail, normalize | ✅ 4/4 |
| 9 | UserFlow u1/u2/imp | u1,u2,imp | add, list, blocked, imp | ✅ 5/5 |
| 10 | Chaos (параллельно) | — | 40 ops, 4 компании | ✅ 1/1 |
Ключевые проверки
| Проверка | Сценарий | Результат |
|---|---|---|
| created_by = originalUserEmail при имперсонации | imp add | ✅ real@t.ru |
| created_by = email админа БЕЗ имперсонации | adm add | ✅ admin@t.ru |
| impersonated_by в аудите | imp audit | ✅ admin@t.ru |
| user_email в аудите = originalUserEmail | imp audit | ✅ real@t.ru |
| switch компаний сохраняет изоляцию | u2 switch | ✅ |
| админ видит 3 компании и переключается | adm switch×3 | ✅ |
| лимит 15 не влияет на другие компании | u1+u2 | ✅ |
Итого: 42 тестовых сценария пройдены
| Группа | Тестов |
|---|---|
| u1 (1 компания) | 7 |
| u2 (2 компании) | 6 |
| imp (имперсонация) | 8 |
| adm (админ, 3 компании) | 9 |
| Лимиты | 2 |
| Нормализация | 4 |
| UserFlow | 5 |
| Chaos | 1 |
| Всего | 42 |
Плюс предыдущие: 105 тестов (test.sh + router + ранние). Общий итог: 147 тестов, все пройдены ✅
Что дальше
| # | Задача | Статус |
|---|---|---|
| 1 | export/ — выгрузка CIDR | ❌ |
| 2 | EJS-шаблоны вместо inline HTML | ❌ |
| 3 | Auth end-to-end (KC callback) | ❌ |
| 4 | Интеграция в основной код (убрать v2_) | ❌ |
Интеграция с основным проектом (0.5.98, 2026-06-15)
Что изменилось
1. Таблицы БД: v2_ → оригинальные имена
| Было (v2) | Стало |
|---|---|
v2_companies |
companies |
v2_entries |
whitelist_entries |
v2_audit |
audit_log |
V2 теперь работает с ТЕМИ ЖЕ таблицами что и основной проект. Старые v2_* таблицы больше не создаются.
Файлы: v2/src/db/schema.js, v2/src/db/queries.js
2. Модуль impersonation/ (новый слой)
Файл: v2/src/impersonation/index.js
Зачем: когда админ имперсонирует в tazet@narod.ru, у этого юзера
должна быть не только своя компания но и админская.
ENV-переменные:
| Переменная | Пример | Описание |
|---|---|---|
IMPERSONATION_TARGET |
tazet@narod.ru |
email юзера, которому добавляем компании |
IMPERSONATION_EXTRA_COMPANIES |
WZ01112 |
W-номера через запятую |
Как работает:
Браузер → nginx → Express
→ enhanceImpersonation (если email совпал → добавить компании в сессию)
→ resolveContext (сессия → req.clientId, req.allClientIds)
→ userRouter / adminRouter
Результат: tazet@narod.ru видит в выпадающем списке свою компанию + WZ01112.
Монтаж в server.js:
router.use('/app', enhanceImpersonation, resolveContext, createUserRouter());
router.use('/admin', enhanceImpersonation, resolveContext, createAdminRouter());
3. Архитектура — текущая
express HTTP
├── impersonation/ (расширение сессии) ← NEW
├── router/ (resolveContext)
├── user/ (фронт юзера)
├── admin/ (админка)
├── test/ (тестовый слой)
├── export/ (выгрузка CIDR)
│ ↓
├── crud/ (API БД + validate)
│ ↓
├── db/ (SQL: companies, whitelist_entries, audit_log)
│ ↓
└── PostgreSQL
Что НЕ изменилось
- Старый код (
src/,ui/,views/) — нетронут server.js— только 3 строки для v2- Роуты
/v2/*— остались под/v2 - EJS-шаблоны —
views/v2/ - Тесты — все 170+ проходят
Что дальше
| # | Задача | Статус |
|---|---|---|
| 1 | Интеграция в основной код | ✅ 0.5.98 |
| 2 | Auth (KC callback) | ⏸ заблокирован облаком |
| 3 | Убрать старые v2_* таблицы из БД | ⏸ после деплоя |
Интеграция v2 в основной проект (0.6.0–0.6.8, 2026-06-15)
Что сделано
| Версия | Что |
|---|---|
| 0.5.98 | Таблицы v2_* → companies, whitelist_entries, audit_log |
| 0.5.98 | impersonation/ модуль — добавление компаний через ENV |
| 0.5.99 | V2 как основной UI: app.use('/', createV2Router()), старый UI закомментирован |
| 0.6.0 | Дизайн Nubes: лого, header, стили из views/index.ejs |
| 0.6.1 | Фикс: email в header |
| 0.6.2 | displayEmail справа от статистики |
| 0.6.3 | Жёлтый баннер имперсонации вместо метки (имперсонация) |
| 0.6.4 | Авто-имперсонация: IMPERSONATION_ORIGINAL → TARGET |
| 0.6.4 | ADMIN_EMAIL в resolveContext |
| 0.6.5 | loginEmail из originalUserEmail |
| 0.6.6 | req.displayEmail — разделение экран/аудит |
| 0.6.7 | Перепутаны местами: req.email и req.impersonatedBy |
| 0.6.8 | «Выйти» — только реальный KC-юзер |
Ключевая ошибка: перепутаны req.email и req.impersonatedBy
Что было неправильно (0.6.6 и ранее):
req.email = реальный → created_by = ntazetdinov@nubes.ru ❌
req.impersonatedBy = имперсонированный → impersonated_by = tazet@narod.ru ❌
Почему ошибался: AI думал что req.email должно хранить реального юзера «для аудита»,
а имперсонированного — для показа. Это неверно. Везде должен использоваться ТЕКУЩИЙ
юзер (имперсонированный если есть имперсонация), а реальный — ТОЛЬКО в колонке
impersonated_by таблицы аудита.
Как правильно (0.6.7+):
req.email = ТЕКУЩИЙ → created_by = tazet@narod.ru ✅
req.impersonatedBy = РЕАЛЬНЫЙ → impersonated_by = ntazetdinov@... ✅
Урок: req.email — это ВСЕГДА текущий юзер. Имперсонация не меняет его смысл —
она меняет значение session.user.email, и req.email просто берёт его.
Правильная логика (0.6.8)
KC логин: ntazetdinov@nubes.ru
↓
session.user.email = ntazetdinov@nubes.ru
↓
enhanceImpersonation: ORIGINAL совпал → авто-имперсонация
session.user.originalUserEmail = ntazetdinov@nubes.ru (реальный)
session.user.email = tazet@narod.ru (текущий)
↓
resolveContext:
req.email = u.email = tazet@narod.ru (текущий — created_by, экран)
req.impersonatedBy = u.originalUserEmail (реальный — только аудит)
req.isImpersonated = true
↓
user/index.js → шаблон:
loginEmail = ntazetdinov@nubes.ru → «Выйти»
email = tazet@narod.ru → контент, статистика
Переменные ENV для авто-имперсонации
| Переменная | Значение | Роль |
|---|---|---|
IMPERSONATION_ORIGINAL |
ntazetdinov@nubes.ru |
Условие: чей email сравнить |
IMPERSONATION_TARGET |
tazet@narod.ru |
Цель: в кого имперсонировать |
IMPERSONATION_COMPANY |
WZ01325 |
Компания: какая компания |
IMPERSONATION_EXTRA_COMPANIES |
WZ01112 |
Доп. компании |
ADMIN_EMAIL |
tazet@narod.ru |
Псевдо-админ |
Все три (ORIGINAL, TARGET, COMPANY) должны быть заданы — иначе авто-имперсонация не включается.