Files
IPWhiteList/docs/architecture.md
T

170 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IP WhiteList — Архитектура (актуально, 2026-05-30 14:43)
> Ветка `sonnet`, репо: `gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git`
> Код: `/home/naeel/ipwhitelist-app`
---
## Стек
| Слой | Технология |
|---|---|
| Сервер | Node.js + Express 4 |
| Шаблоны | EJS (server-side, без SPA) |
| БД | PostgreSQL, клиент `pg` (pool) |
| Безопасность | helmet, express-rate-limit, csrf-csrf, express-session |
| JWT | jsonwebtoken (mock RS256 / OIDC validation) |
| Авторизация | Keycloak (OIDC) или mock при локальной разработке |
---
## Структура модулей
```
server.js — точка входа: init + подключение роутеров (131 строк)
src/
auth.js — аутентификация: OIDC или mock, session middleware
config.js — MOCK_USERS, backUrl()
db.js — pg pool (checkConnection)
queries.js — все SQL-запросы к БД
validators.js — validateCIDR, aggregateCIDRs
middleware/
csrf.js — initCsrf() → { doubleCsrfProtection, generateCsrfToken }
rateLimit.js — mutationLimiter (30/мин), exportLimiter (20/мин)
routes/
auth.js — /login /callback /logout /dev-login
entries.js — / /add /edit/:id /delete/:id
admin.js — /audit /admin /admin/limit/:companyId
export.js — /export
views/
login.ejs — форма входа (mock) или заглушка (OIDC)
dev-login.ejs — тестовый вход (пресеты + произвольные поля)
index.ejs — список записей пользователя / admin-обзор
admin.ejs — admin-панель (все компании, лимиты)
audit.ejs — лог операций
sql/
schema.sql — companies, whitelist_entries (soft delete), audit_log
```
---
## Схема авторизации
```
GET /login
┌─ OIDC-режим (KC_CLIENT_ID задан) ──────────────────────────────────────┐
│ state → сессия │
│ redirect → keycloak.nubes.ru/realms/cloud/.../auth │
│ ↓ KK перенаправляет на /callback │
│ GET /callback → проверить state → exchangeCode → токен KK │
│ → userFromPayload → req.session.user → redirect / │
└─────────────────────────────────────────────────────────────────────────┘
┌─ Mock-режим (KC_CLIENT_ID не задан) ───────────────────────────────────┐
│ render login.ejs (выбрать из MOCK_USERS) │
│ POST /login → CSRF → req.session.user → redirect / │
└─────────────────────────────────────────────────────────────────────────┘
GET /dev-login (DEV_MODE=true или DEV_SECRET задан)
render dev-login.ejs (пресеты MOCK_USERS + произвольные поля)
POST /dev-login → CSRF → req.session.user → redirect /
⚠ Работает в ОБОИХ режимах — для тестирования с разными ролями
GET /logout
session.destroy() + clearCookie('connect.sid')
OIDC: redirect → keycloak logout endpoint
Mock: redirect → /login
Все защищённые роуты: auth.middleware
req.session.user → req.user (основной путь)
Authorization: Bearer → verify JWT → req.user (API-клиенты)
cookie 'jwt' (legacy) → verify → session → req.user (плавная миграция)
нет ничего → GET: redirect /login, остальное: 401
```
---
## JWT claims (auth-api, из реального токена портала)
> auth-api выпускает свой JWT — **не Keycloak напрямую**.
> Issuer: `"auth-api"`, алгоритм: RS256.
> Наш сервис принимает и валидирует этот токен.
| Поле | Тип | Пример | Что используем |
|---|---|---|---|
| `ClientID` | string | `"WZ01325"` | `req.user.clientId` |
| `company_id` | UUID | `"3e64aac6-..."` | `req.user.companyId`, FK в БД |
| `company_name` | string | `"Тест"` | `req.user.companyName` |
| `email` | string | `"user@example.com"` | `req.user.email` (аудит) |
| `login` | string | `"user@example.com"` | fallback для email |
| `sub` | UUID | `"0199e325-..."` | fallback для companyId |
| `iss` | string | `"auth-api"` | проверяется при валидации |
| `exp` | number | ~12 часов | автоматически |
**Признак admin:** `clientId === ADMIN_CLIENT_ID` (env `ADMIN_CLIENT_ID`, default `WZ01112`).
`isAdmin` не приходит в JWT — см. [questions.md](questions.md).
---
## Маршруты
| Метод | Путь | Доступ | Лимит |
|---|---|---|---|
| GET | `/healthz` | public | — |
| GET | `/.well-known/jwks.json` | public | — |
| GET | `/export` | public | 20/мин |
| GET/POST | `/login` | public | — |
| GET | `/callback` | public | — |
| GET | `/logout` | public | — |
| GET/POST | `/dev-login` | public (с guard) | — |
| GET | `/` | auth | — |
| POST | `/add` `/edit/:id` `/delete/:id` | auth | 30/мин |
| GET | `/audit` `/admin` | auth + admin | — |
| POST | `/admin/limit/:companyId` | auth + admin | 30/мин |
---
## Env-переменные
### Обязательные в продакшене
| Переменная | Описание |
|---|---|
| `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASS` | PostgreSQL |
| `SESSION_SECRET` | Секрет сессии (≥ 32 символа) |
| `CSRF_SECRET` | Секрет CSRF (≥ 32 символа) |
| `KC_CLIENT_ID` | client_id в Keycloak realm cloud |
| `KC_CLIENT_SECRET` | client_secret |
| `APP_URL` | Внешний URL приложения (для redirect_uri) |
### Опциональные
| Переменная | Default | Описание |
|---|---|---|
| `KC_BASE_URL` | `https://keycloak.nubes.ru/realms/cloud` | Keycloak realm base URL |
| `ADMIN_CLIENT_ID` | `WZ01112` | WZ-номер администратора |
| `PORT` | `3000` | HTTP-порт |
| `NODE_ENV` | — | `production` включает secure cookie, trust proxy |
| `DEV_MODE` | `false` | `true` → /dev-login без пароля |
| `DEV_SECRET` | — | Ключ для /dev-login в staging |
---
## БД (dev)
```
Host: write.bde8229b-1381-4330-b24b-727ad73fcb44.dev.nubes.ru
DB: ipwhitelist
User: super
```
Схема: `sql/schema.sql``companies`, `whitelist_entries` (soft delete), `audit_log` + индексы.
---
## Деплой (текущий)
- URL: `https://white.nodejsk8s.dev.nubes.ru`
- k8s namespace: `whitelist`
- Сессии: in-memory (MemoryStore) — **не масштабируется**, нужен Redis при multi-pod
- Режим: ожидает KC_CLIENT_ID/KC_CLIENT_SECRET для перехода с mock на OIDC