# 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.5.0`) |

**Правило версий:** 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. Переменные окружения

```env
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):

```js
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. База данных — схема

```sql
-- Компании (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 — функции

```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()

```js
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 — интерфейс

```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 | email |
|---|---|---|---|---|
| `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. Бизнес-логика — важные правила

1. **Изоляция компаний**: везде `company_id` в WHERE — данные одной компании недоступны другой.
2. **Транзакции с FOR UPDATE**: createEntry/updateEntry/deleteEntry блокируют строку компании — защита от race condition при параллельных вставках.
3. **Soft delete**: `deleted_at IS NULL` = активная запись. Удалённые остаются в БД.
4. **Дубли/пересечения**: при создании и обновлении проверяются через `overlaps()` против всех активных записей компании.
5. **Лимит**: снижение лимита не удаляет записи — только блокирует создание новых.
6. **CIDR нормализация**: `validate()` нормализует host bits (192.168.1.5/24 → 192.168.1.0/24), возвращает `wasNormalized: true`.
7. **Агрегация при экспорте**: `aggregateCIDRs()` объединяет смежные диапазоны — минимальный набор.

---

## 17. HTTP коды ошибок API

| Код | Когда |
|---|---|
| 400 | Невалидный CIDR, некорректные параметры |
| 401 | Нет/невалидный Bearer токен |
| 403 | Не admin на admin-endpoint |
| 404 | Запись/компания не найдена |
| 409 | Дубликат CIDR, пересечение, лимит исчерпан |
| 500 | Ошибки БД и прочие |

---

## 18. Запуск и тесты

```bash
# Установка
npm install

# Запуск локально (нужен .env с DEV_MODE=true + БД)
node server.js

# API-тесты (104 штуки — CRUD, валидация, изоляция, admin, лимиты)
npm test

# Стресс-тесты (191 штука — concurrency, races, boundary IPs, masks, auth attacks)
npm run test:stress

# Старый UI-тест (неактуален, использует /dev-login + CSRF)
npm run test:integration
```

**Все тесты требуют реального PG подключения** (переменные из `.env`).

### Структура тестов (`tests/stress.js`):

| Раздел | Что проверяет |
|---|---|
| S1 | Concurrency: FOR UPDATE lock, 10-15 параллельных вставок |
| S2 | Limit enforcement под нагрузкой, setLimit(0) |
| S3 | Mixed CRUD: concurrent update+delete+create |
| S4 | Auth attacks: fake key, none algorithm, missing claims |
| S5 | Edge cases: emoji, null byte, null/''/undefined |
| S6 | Validator: overlaps, 14 blocked ranges |
| S7 | Session vs Bearer conflict (bearerMiddleware) |
| S8 | All CIDR masks /0–/33 (34 теста) |
| S9 | IP boundary values: 0.0.0.0 … 255.255.255.255 (32 теста) |
| S10 | Token manipulation: expired, wrong issuer, future, no claims |
| S11 | DB: double delete, update deleted, isolation |
| S12 | Unicode: full-width, zero-width, RTL, NFC/NFD |
| S13 | Admin privilege escalation |
| S14 | Session: login→logout, cookie reuse |
| S15 | Rate limit /exp, /login |
| S16 | Export edge cases |
| S17 | Massive concurrency: 50 createEntry, 30 delete, 20 getOrCreate |
| S18 | CIDR normalization: host bits обнуление |
| S19 | Each blocked range individually (14) |
| S20 | Sequential CRUD patterns |
| S21 | Admin cross-company CRUD |
| S22 | Weird CIDR input: пробелы, табы, \n, 256, IPv6 (20) |

---

## 19. Известные проблемы (баги) — исправлены в v0.5.0

| Проблема | Статус |
|---|---|
| `auth.middleware` читал `req.session.user` перед Bearer → Bearer-only API нарушалось | ✅ Добавлен `bearerMiddleware` |
| Mock admin `isAdmin: undefined` в сессии | ✅ `user.role === 'admin'` |
| Сброс лимита (пустое поле → NaN → 400) | ✅ `null` при пустом поле |
| `setLimit()` не проверял `rowCount` → 404 на неизвестную компанию | ✅ 404 с `e.status` |
| Версия не поднималась при правках | ✅ `0.4.1` → `0.5.0` (9 бампов) |
| CSP/ingress блокировал inline-обработчики (onclick, onchange) | ✅ Event delegation через `<script>` |
| Admin при `/` редиректился на первую компанию, но не на свою | ✅ Авто-редирект на компанию admin по `clientId` |
| Даты в UTC вместо МСК | ✅ `timeZone: 'Europe/Moscow'` |
| Фильтр аудита: `onchange` ненадёжен | ✅ Кнопка «Применить» + `Number()` сравнение |
| `/admin` → 500 из-за отсутствия `defaultLimit` в шаблоне | ✅ Передаётся в `render()` |

## 20. ⚠️ ВРЕМЕННЫЕ ЭЛЕМЕНТЫ — удалить перед продом

### GET /exp — публичный экспорт без авторизации (добавлен v0.4.9)

**Причина:** По ТЗ п. 4.7 `/export` должен быть публичным, но сейчас требует авторизацию. Создан временный `/exp` как workaround.

**Что удалить:**
1. Файл `src/routes/exp.js` — полностью
2. В `server.js` строку: `app.use(require('./src/routes/exp'));`

**Когда:** когда `/export` сделают публичным по ТЗ.

---

## 21. Расхождения с ТЗ (открытые задачи)

| Пункт ТЗ | Суть | Статус |
|---|---|---|
| 3.1 | Несколько компаний на пользователя | Не реализовано — ждём devops |
| 4.1 | Отображение soft-deleted для admin | Не реализовано |
| 4.7 | `/export` без авторизации | Обход: `/exp` (временный) |
