18 KiB
18 KiB
AGENT.md — Полное руководство по проекту IPWhiteList
Этот файл предназначен для AI-агентов и разработчиков без контекста.
Читай этот файл первым перед любой задачей.
Обновляй после значимых изменений.
1. Суть проекта
IP WhiteList — веб-приложение для управления белыми IP-списками (CIDR) по компаниям.
- Каждая компания имеет свой список IPv4/CIDR записей.
- Есть лимит записей на компанию (по умолчанию 15, admin может менять).
- Есть одна роль admin (задаётся через env) и обычные пользователи.
- Аутентификация через JWT (RS256): в проде — внешний OIDC (Keycloak/Nubes auth-api), в dev — mock.
- Весь UI — server-side rendered (EJS). API — REST JSON.
2. Репозиторий и деплой
| Параметр | Значение |
|---|---|
| Репо (рабочий) | https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git ветка master |
| Локальный путь | /home/naeel/ipwhitelist-app |
| Деплой URL | https://white.nodejsk8s.dev.nubes.ru |
| Платформа | Nubes k8s — редеплой через Nubes UI из Gitea после git push |
| Версия | package.json → поле version (сейчас 0.4.9) |
Правило версий: bump version в package.json при каждом значимом изменении.
3. Стек
Node.js 18+ + Express 4
EJS (server-side rendering)
PostgreSQL 17 (pg pool, SSL require)
jsonwebtoken RS256
express-session + connect-pg-simple
helmet (CSP)
express-rate-limit
supertest (тесты)
4. Переменные окружения
NODE_ENV=development # production → отключает DEV_MODE, требует SESSION_SECRET
PORT=3000
# Auth
DEV_MODE=true # true → mock JWT, ЗАПРЕЩЕНО в production
DEV_SECRET= # опционально: пароль для /dev-login на staging
ADMIN_CLIENT_ID=WZ01112 # clientId пользователя с правами admin
JWT_ISSUER=mock-auth-api # issuer в JWT
# OIDC (если DEV_MODE=false)
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
KC_CLIENT_ID= # пусто = mock mode
KC_CLIENT_SECRET=
APP_URL=http://localhost:3000
# База данных
DB_HOST=write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru
DB_PORT=5432
DB_NAME=ipwhitelist
DB_USER=super
DB_PASS=<secret>
DB_SSLMODE=require
# Сессия
SESSION_SECRET=<secret> # обязателен в production (fail-fast при запуске)
# Прочее
DEFAULT_LIMIT=15 # лимит записей по умолчанию (если нет custom_limit)
API_BASE=http://localhost:3000 # UI-слой → куда идти за /api/v1/*
5. Структура файлов — карта ответственности
server.js ← точка входа: helmet, session, монтирование роутеров
src/
config.js ← MOCK_USERS, APP_VERSION, backUrl()
db.js ← pg Pool, checkConnection()
auth.js ← initAuth() → { middleware, issueMockToken, ... }
queries.js ← ВСЕ SQL-запросы — единственное место работы с БД
validators.js ← validate(cidr), overlaps(), aggregateCIDRs(), BLOCKED_RANGES
api/
index.js ← createApiRouter({auth, q}) — монтируется на /api/v1
middleware/
bearerAuth.js ← requireBearer: нет заголовка → 401 JSON
routes/
entries.js ← GET/POST/PATCH/DELETE /api/v1/entries + GET /api/v1/entries/export
admin.js ← GET /api/v1/companies, PATCH limit, GET /api/v1/audit
middleware/
rateLimit.js ← mutationLimiter, exportLimiter, authLimiter
session.js ← createSessionMiddleware(pool)
csrf.js ← initCsrf() — НЕ используется в новом UI
csp.js ← CSP_DIRECTIVES для helmet
routes/
export.js ← GET /export — требует авторизации (Bearer)
exp.js ← ⚠️ ВРЕМЕННЫЙ GET /exp — без авторизации (удалить после prod)
ui/
index.js ← createUiRouter({auth, MOCK_USERS, authLimiter})
api-client.js ← HTTP-клиент к /api/v1/* (берёт Bearer из session.token)
routes/
auth.js ← GET/POST /login, /login-token, /logout
entries.js ← /, /add, /edit/:id, /delete/:id
admin.js ← /admin, /admin/limit/:id, /audit
export.js ← /export (UI-обёртка, проксирует к /api/v1/entries/export)
views/
index.ejs ← главная: таблица записей + форма добавления
admin.ejs ← список компаний с лимитами (admin only)
audit.ejs ← журнал аудита (admin only)
ui-login.ejs ← страница входа (mock-пресеты + вставить Bearer вручную)
error.ejs ← 403/404/500
sql/
schema.sql ← DDL: CREATE TABLE IF NOT EXISTS (идемпотентно)
tests/
integration.js ← интеграционные тесты (supertest + реальный PG)
run-tests.js ← runner
6. Поток запроса — HTTP → БД
6.1 REST API запрос
HTTP Request
→ server.js: helmet, session, cookieParser
→ GET /exp → src/routes/exp.js (без auth, временный) ← ТОЛЬКО ЭТО без auth
→ /api/v1/* → src/api/index.js
→ requireBearer : нет заголовка Authorization: Bearer → 401 JSON
→ auth.middleware : валидация JWT → req.user = { clientId, isAdmin, email, ... }
→ /entries/* → src/api/routes/entries.js
→ resolveCompany() : admin+?company=id → getCompanyById() | иначе getOrCreateCompany()
→ q.*() : SQL через pg pool
→ JSON response
→ /companies, /audit → src/api/routes/admin.js
→ apiRequireAdmin() : !isAdmin → 403 JSON
→ q.*()
6.2 UI запрос (SSR)
HTTP Request
→ server.js → ui/index.js (createUiRouter)
→ createAuthRouter : /login, /login-token, /logout — публичные
→ requireToken : нет session.token → redirect /login
→ resolveUser : jwt.decode(token) → req.user (без верификации подписи)
→ createEntriesRouter, createAdminRouter, createExportRouter
→ ui/api-client.js : HTTP запрос к /api/v1/* с Bearer из session.token
→ разбирает JSON ответ
→ res.render('view.ejs', data)
→ Если API вернул 401 → destroy session → redirect /login
7. req.user — структура
После прохождения auth.middleware (API) или resolveUser (UI):
req.user = {
clientId: 'WZ01112', // из JWT claim ClientID (заглавная!)
companyId: 'uuid-...', // из JWT claim company_id
companyName: 'Nubes Admin', // из JWT claim company_name
email: 'admin@nubes.ru', // из JWT claim email
isAdmin: true, // clientId === ADMIN_CLIENT_ID
}
8. База данных — схема
-- Компании (1 строка на WZ-номер, создаётся при первом входе)
companies (
id SERIAL PRIMARY KEY,
client_id VARCHAR(64) UNIQUE NOT NULL, -- WZ01112, WZ01325 и т.д.
name VARCHAR(255),
custom_limit INTEGER DEFAULT NULL, -- NULL = использует DEFAULT_LIMIT
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
)
-- Записи белого списка (soft delete через deleted_at)
whitelist_entries (
id SERIAL PRIMARY KEY,
company_id INTEGER REFERENCES companies(id),
value_cidr VARCHAR(18), -- '192.168.1.0/24'
comment VARCHAR(255),
created_by VARCHAR(255), -- email
created_at TIMESTAMPTZ,
updated_by VARCHAR(255),
updated_at TIMESTAMPTZ,
deleted_by VARCHAR(255),
deleted_at TIMESTAMPTZ -- NULL = активная запись
)
-- INDEX: uq_entries_active_cidr(company_id, value_cidr) WHERE deleted_at IS NULL
-- Аудит
audit_log (
id SERIAL PRIMARY KEY,
user_email VARCHAR(255),
company_id INTEGER,
action VARCHAR(32), -- 'CREATE' | 'UPDATE' | 'DELETE'
old_value TEXT,
new_value TEXT,
entry_id INTEGER,
created_at TIMESTAMPTZ
)
9. src/queries.js — все функции
| Функция | Сигнатура | Описание |
|---|---|---|
getOrCreateCompany |
(clientId, companyName) → company |
UPSERT компании по clientId |
getCompanyById |
(id) → company|null |
по числовому PK |
getLimit |
(company) → number |
custom_limit ?? DEFAULT_LIMIT |
listEntries |
(companyId, includeDeleted=false) → [] |
список записей |
createEntry |
(companyId, rawCidr, comment, email) → {entry, wasNormalized} |
INSERT + audit (транзакция) |
updateEntry |
(entryId, companyId, rawCidr, comment, email) → {entry, wasNormalized} |
UPDATE + audit (транзакция) |
deleteEntry |
(entryId, companyId, email) → void |
soft delete + audit (транзакция) |
getAllCompanies |
() → [] |
все компании + active_count |
setLimit |
(companyId, newLimit) → void |
custom_limit = newLimit |
getExportCIDRs |
(companyId=null) → string[] |
CIDR строки; null = все компании |
logAudit |
(email, companyId, action, old, new, entryId, db?) → void |
внутренняя, вызывается из транзакций |
getAudit |
(companyId=null) → [] |
журнал с JOIN companies |
10. src/validators.js — функции
validate(rawValue)
→ { cidr: '1.2.3.0/24', wasNormalized: boolean }
→ throws Error если невалидно
// Нормализует host bits, проверяет BLOCKED_RANGES
overlaps(cidr1, cidr2) → boolean
// true если диапазоны пересекаются
aggregateCIDRs(cidrArray) → string[]
// Объединяет пересекающиеся и смежные диапазоны → минимальный набор
BLOCKED_RANGES // приватные/зарезервированные диапазоны — запрещены для добавления
11. src/auth.js — initAuth()
const auth = await initAuth();
// Возвращает:
auth.middleware // Express middleware: валидирует Bearer JWT → req.user
auth.issueMockToken(claims) → jwtString // только в mock-режиме, для тестов
auth.jwksHandler // GET /.well-known/jwks.json (только mock)
// OIDC-режим:
auth.getAuthUrl(state, nonce) → string // redirect URL на Keycloak
auth.exchangeCode(code) → {token, user} // обмен code → JWT
Логика определения режима:
KC_CLIENT_ID && KC_CLIENT_SECRET→ OIDC-режим- иначе → mock-режим (генерирует RSA-пару при старте)
12. ui/api-client.js — интерфейс
api.token(req) // → Bearer string из req.session.token
api.get(path, token) // → { status, data }
api.post(path, token, body) // → { status, data }
api.patch(path, token, body) // → { status, data }
api.delete(path, token) // → { status, data }
// path примеры: '/api/v1/entries', '/api/v1/entries/42', '/api/v1/companies/5/limit'
13. Все HTTP эндпоинты
Публичные (без auth)
| Метод | URL | Файл | Описание |
|---|---|---|---|
| GET | /healthz |
server.js | k8s liveness probe |
| GET | /.well-known/jwks.json |
server.js | mock JWKS (только DEV_MODE) |
| GET | /login |
ui/routes/auth.js | страница входа |
| POST | /login |
ui/routes/auth.js | выбор mock-пользователя |
| POST | /login-token |
ui/routes/auth.js | вставить Bearer вручную |
| GET | /logout |
ui/routes/auth.js | destroy session |
| GET | /callback |
ui/routes/auth.js | OIDC callback |
| GET | /exp |
src/routes/exp.js | ⚠️ ВРЕМЕННЫЙ: экспорт без auth |
UI (требуют session.token)
| Метод | URL | Файл | Описание |
|---|---|---|---|
| GET | / |
ui/routes/entries.js | главная страница |
| POST | /add |
ui/routes/entries.js | добавить запись |
| POST | /edit/:id |
ui/routes/entries.js | изменить запись |
| POST | /delete/:id |
ui/routes/entries.js | удалить запись |
| GET | /admin |
ui/routes/admin.js | список компаний (admin) |
| POST | /admin/limit/:id |
ui/routes/admin.js | изменить лимит (admin) |
| GET | /audit |
ui/routes/admin.js | журнал аудита (admin) |
| GET | /export |
ui/routes/export.js | скачать whitelist.txt |
REST API /api/v1 (требуют Bearer JWT)
| Метод | URL | Файл | Кто | Описание |
|---|---|---|---|---|
| GET | /api/v1/entries |
api/routes/entries.js | все | список + limit + used |
| GET | /api/v1/entries?company=<id> |
api/routes/entries.js | admin | записи другой компании |
| POST | /api/v1/entries |
api/routes/entries.js | все | создать |
| PATCH | /api/v1/entries/:id |
api/routes/entries.js | все | обновить |
| DELETE | /api/v1/entries/:id |
api/routes/entries.js | все | удалить |
| GET | /api/v1/entries/export |
api/routes/entries.js | все | text/plain CIDR |
| GET | /api/v1/companies |
api/routes/admin.js | admin | все компании |
| PATCH | /api/v1/companies/:id/limit |
api/routes/admin.js | admin | установить лимит |
| GET | /api/v1/audit |
api/routes/admin.js | admin | журнал аудита |
14. Rate limiting
| Лимитер | Применяется к | Лимит |
|---|---|---|
authLimiter |
POST /login, /login-token, GET /callback | 10 req / 5 мин |
exportLimiter |
GET /export, GET /exp, GET /api/v1/entries/export | 20 req / мин |
mutationLimiter |
POST /add, /edit/:id, /delete/:id | 30 req / мин |
15. Mock-пользователи (DEV_MODE=true)
| id | clientId | Роль | companyName | |
|---|---|---|---|---|
admin |
WZ01112 |
admin | Nubes Admin | admin@nubes.ru |
test |
WZ01325 |
user | Тест | tazet@narod.ru |
client2 |
WZ02001 |
user | Вторая Компания | user2@example.com |
Файл: src/config.js → MOCK_USERS
16. Бизнес-логика — важные правила
- Изоляция компаний: везде
company_idв WHERE — данные одной компании недоступны другой. - Транзакции с FOR UPDATE: createEntry/updateEntry/deleteEntry блокируют строку компании — защита от race condition при параллельных вставках.
- Soft delete:
deleted_at IS NULL= активная запись. Удалённые остаются в БД. - Дубли/пересечения: при создании и обновлении проверяются через
overlaps()против всех активных записей компании. - Лимит: снижение лимита не удаляет записи — только блокирует создание новых.
- CIDR нормализация:
validate()нормализует host bits (192.168.1.5/24 → 192.168.1.0/24), возвращаетwasNormalized: true. - Агрегация при экспорте:
aggregateCIDRs()объединяет смежные диапазоны — минимальный набор.
17. HTTP коды ошибок API
| Код | Когда |
|---|---|
| 400 | Невалидный CIDR, некорректные параметры |
| 401 | Нет/невалидный Bearer токен |
| 403 | Не admin на admin-endpoint |
| 404 | Запись/компания не найдена |
| 409 | Дубликат CIDR, пересечение, лимит исчерпан |
| 500 | Ошибки БД и прочие |
18. Запуск и тесты
# Установка зависимостей
npm install
# Запуск локально (нужен .env)
node server.js
# Тесты (supertest + реальная БД)
npm test
# или
node tests/run-tests.js
Тесты требуют реального PG подключения (переменные из .env).
19. ⚠️ ВРЕМЕННЫЕ ЭЛЕМЕНТЫ — удалить перед продом
GET /exp — публичный экспорт без авторизации (добавлен v0.4.9)
Причина: По ТЗ п. 4.7 /export должен быть публичным, но сейчас требует авторизацию. Создан временный /exp как workaround.
Что удалить:
- Файл
src/routes/exp.js— полностью - В
server.jsстроку:app.use(require('./src/routes/exp'));
Когда: когда /export сделают публичным по ТЗ.
20. Расхождения с ТЗ (открытые задачи)
| Пункт ТЗ | Суть | Статус |
|---|---|---|
| 3.1 | Несколько компаний на пользователя | Не реализовано — ждём devops |
| 4.1 | Отображение soft-deleted для admin | Не реализовано |
| 4.7 | /export без авторизации |
Обход: /exp (временный) |