v0.5.14 — вход по токену, JWKS в mock-режиме, production deploy docs

- POST /login-token: принимает реальный JWT (не только clientId)
- auth.js: verifyAnyToken — mock + JWKS (Keycloak Nubes) одновременно
- auth.js: JWKS предзагружается всегда, даже в mock-режиме
- devops-deploy.md: полная production-инструкция для DevOps
- .env.example: production-ready шаблон
- README.md: актуальный (v0.5.13, архитектура, тесты, ссылки)
- .gitignore: очищен от дубликатов
This commit is contained in:
2026-06-03 12:37:10 +03:00
parent 22beaa86a3
commit 38c7adf25d
7 changed files with 396 additions and 143 deletions
+9 -7
View File
@@ -2,15 +2,17 @@
DB_HOST=localhost
DB_PORT=5432
DB_NAME=ipwhitelist
DB_USER=super
DB_PASS=сменить_пароль
DB_USER=ipwhitelist
DB_PASS=<openssl rand -base64 24>
DB_SSLMODE=disable
# ── Режим разработки (true = без Keycloak, mock-логин) ──
# ── Режим разработки ──
# true = mock-логин (без Keycloak, для тестов)
# false = OIDC SSO через Keycloak (ОБЯЗАТЕЛЬНО в production)
DEV_MODE=true
DEFAULT_LIMIT=15
# ── Сессия (сменить на случайные строки) ──
# ── Сессия (сгенерировать: openssl rand -base64 32) ──
SESSION_SECRET=сменить_на_случайную_строку
CSRF_SECRET=сменить_на_случайную_строку
@@ -20,8 +22,8 @@ PORT=3001
# ── Администратор (clientId) ──
ADMIN_CLIENT_ID=WZ01112
# ── Keycloak (оставить пустым пока нет OIDC) ──
# KC_CLIENT_ID=
# KC_CLIENT_SECRET=
# ── Keycloak / OIDC (ЗАПОЛНИТЬ при DEV_MODE=false) ──
# KC_CLIENT_ID= # ID зарегистрированного OIDC-клиента в Keycloak
# KC_CLIENT_SECRET= # секрет клиента
# KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
# APP_URL=https://ваш-домен.ru
+10 -24
View File
@@ -1,41 +1,27 @@
.env
# Система
node_modules/
.env
__pycache__/
*.pyc
.DS_Store
.vscode/
.idea/
Files/nubes_login.har
# Креды, токены, конфиги
token*.*
Files/
kubeconfig-*
# Внутренняя аналитика не для публичного доступа
PROJECT-AUDIT.md
AGENT-DIAGNOSIS.md
# Внутренняя аналитика (не для публичного доступа)
AGENT.md
AGENT-DIAGNOSIS.md
PROJECT-AUDIT.md
CONTEXT.md
prompt-opus-*.md
docs/questions-devops.md
docs/PLAN-*.md
docs/STATE-*.md
docs/cluster-cleanup-*.md
docs/tests-*.md
docs/test-api.sh
docs/nubes-design-system.md
docs/deploy-vm.md
SESSION-SUMMARY.md
prompt-*.md
# Результаты код-ревью агентов
research/
# Тестовые данные (токены, HAR, etc)
Files/
token*.*
# Логи тестов
test-results/
research/opus-full-review-*.md
research/REVIEW-SUMMARY.md
docs/STATE-old.md
# Результаты тестов — генерируются при прогоне
test-results/
+72 -32
View File
@@ -1,57 +1,97 @@
# IP WhiteList
> ⚠️ **В процессе разработки.** НЕ для production.
# IP WhiteList v0.5.13
Self-service портал для управления доверенными IPv4-адресами клиентов облачного провайдера.
Записи исключаются из блокировки системами фильтрации во время DDoS-атак.
**[Полное техническое задание](docs/ТЗ.md)** · **[Отчёт о соответствии](docs/TZ-COMPLIANCE-REPORT.md)**
**[Техническое задание](docs/ТЗ.md)** · **[DevOps — production deploy](docs/devops-deploy.md)**
---
## Что делает
## Возможности
- **Клиент** авторизуется через Keycloak (OIDC) и управляет списком своих IPv4/CIDR: добавляет, редактирует, удаляет (soft delete)
- **Валидация** на сервере: маски /22/32, запрет 14 приватных/служебных диапазонов, запрет дубликатов и пересечений внутри компании, нормализация host-битов
- **Лимиты:** глобальный (15 по умолчанию, настраивается через env) и индивидуальный на компанию (устанавливает админ)
- **Администратор** видит записи всех компаний, управляет лимитами, просматривает журнал аудита
- **Аудит:** все изменения (создание/редактирование/удаление) фиксируются — кто, когда, что было и что стало
- **Управление белым списком:** добавление, редактирование, удаление (soft delete) IPv4/CIDR
- **Валидация** на сервере: маски `/22``/32`, запрет 14 приватных/служебных диапазонов, запрет дубликатов и пересечений, нормализация host-битов
- **Лимиты:** глобальный (по умолчанию 15) и индивидуальный на компанию (настраивает админ)
- **Агрегированный экспорт** CIDR-списка для систем фильтрации (`GET /export`)
- **Мульти-компания:** один пользователь может управлять несколькими компаниями (clientId через запятую)
- **Аудит:** все изменения фиксируются — кто, когда, что было и что стало
- **Администрирование:** просмотр всех компаний, управление лимитами, журнал аудита
## Роли
## Аутентификация
| Роль | Права |
| Режим | Описание |
|---|---|
| Клиент | CRUD своих записей в пределах лимита |
| Администратор (WZ01112) | CRUD всех записей, изменение лимитов, просмотр аудита |
| **DEV_MODE=true** (тесты/разработка) | Mock JWT, локальная RSA-пара. Вход через форму выбора пользователя или вставку Bearer-токена |
| **DEV_MODE=false** (production) | OIDC SSO через Keycloak Nubes. Редирект на Keycloak login → `/callback` → сессия |
## Как получить список для фильтрации
Агрегированный список всех активных CIDR в формате txt (одна строка — одна подсеть).
| Endpoint | Авторизация | Статус |
|---|---|---|
| `GET /export` | Bearer-токен | Будет основным (публичным по ТЗ) |
| `GET /exp` | **Нет** | Временный, работает сейчас |
После перевода `/export` на публичный доступ `/exp` будет удалён.
В production требуются `KC_CLIENT_ID` + `KC_CLIENT_SECRET` от DevOps. Подробнее — [deploy guide](docs/devops-deploy.md).
## Стек
Node.js 18+ / Express 4 / EJS (SSR) / PostgreSQL 17 / jsonwebtoken RS256
```
Node.js 20+ → Express 4 → EJS (SSR) → PostgreSQL 16+
jsonwebtoken RS256 / JWKS (Keycloak Nubes)
express-session + connect-pg-simple
```
## Архитектура
```
Browser → UI (EJS, сессия) → REST API (Bearer JWT) → PostgreSQL
Browser → nginx:443 → Express:3001
├── /export (публичный CIDR)
├── /api/v1/* (REST API, Bearer JWT)
└── /* (UI, SSR EJS, сессия → api-client.js → API)
```
Два слоя: SSR UI для браузера, JSON API для внешних потребителей.
Два слоя: SSR UI для браузера, JSON API для внешних систем.
## Деплой
## Multi-company
Nubes k8s. Редеплой — Nubes UI из Gitea после `git push`.
URL: `https://white.nodejsk8s.dev.nubes.ru`
Пользователь может принадлежать нескольким компаниям. В JWT claim `ClientID` передаются значения через запятую:
```
ClientID: "WZ11125,WZ03816"
```
Первое значение — активная компания. Переключение компаний — через UI.
## Разработка
## Тесты
Перед любой задачей читать [`AGENT.md`](AGENT.md) — руководство для агентов и разработчиков.
```bash
npm test # 121 API-тест (Bearer JWT, CRUD, изоляция, admin)
node tests/integration.js # 68 UI-тестов (сессии, EJS)
node tests/tz-compliance.js # 47 тестов ТЗ
node tests/tz-full-compliance.js # 111 тестов (20 компаний × 100 CIDR)
node tests/stress.js # 191 стресс-тест (concurrency, auth attacks, edge cases)
```
Все тесты требуют реального PostgreSQL.
## Быстрый старт (dev)
```bash
git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git
cd ipwhitelist-app
cp .env.example .env # заполнить DB_* и SESSION_SECRET
npm install
psql -U postgres -d ipwhitelist -f sql/schema.sql
npm start # http://localhost:3001 + DEV_MODE=true
```
## Production deploy
См. [`docs/devops-deploy.md`](docs/devops-deploy.md) — nginx, HTTPS, PM2, Keycloak SSO.
## Документация
- [Техническое задание](docs/ТЗ.md)
- [Уточнения ТЗ](docs/ТЗ-плюс.md)
- [DevOps deployment guide](docs/devops-deploy.md) (англ.)
- [AI Agent Guide](.github/AGENT-GUIDE.md) (англ., для Copilot)
- [Copilot Instructions](.github/copilot-instructions.md) (правила для AI)
- [Keycloak/OIDC reference](docs/keycloak-auth-reference.md)
- [Deploy Keycloak](docs/deploy-keycloak.md)
## Текущий деплой
- **URL:** `https://italo.kube5s.ru`
- **Режим:** DEV_MODE=true (mock)
- **БД:** PostgreSQL на ВМ, localhost only
+209 -60
View File
@@ -1,73 +1,140 @@
# Деплой ipwhitelist для DevOps
# Деплой ipwhitelist — PRODUCTION
> Репозиторий: `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git`
> Репозиторий: `https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git` (публичный)
> Стек: Node.js 20+ / Express 4 / EJS / PostgreSQL 16+
> Требования: доступ к Keycloak Nubes (realm `cloud`), зарегистрированный OIDC-клиент
## Быстрый старт
---
## 1. Подготовка сервера
```bash
# 1. Установить Node.js v20+
# Node.js 20+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs git
apt install -y nodejs git nginx certbot python3-certbot-nginx postgresql
# 2. Скачать
git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git
cd ipwhitelist-app
npm install
# 3. Создать .env (см. ниже)
cp .env.example .env
nano .env
# 4. Создать БД PostgreSQL
sudo -u postgres psql -c "CREATE USER super WITH PASSWORD '...';"
sudo -u postgres psql -c "CREATE DATABASE ipwhitelist OWNER super;"
psql -h localhost -U super -d ipwhitelist -f sql/schema.sql
# 5. Запустить
npm start
# ИЛИ через PM2:
npm i -g pm2
pm2 start server.js --name ipwhitelist
# Код
git clone https://gitea.services.ngcloud.ru/Nail/ipwhitelist-app.git /opt/ipwhitelist
cd /opt/ipwhitelist
npm install --production
```
## .env (все параметры)
---
## 2. PostgreSQL
```bash
# ── База данных PostgreSQL ──
# Создать пользователя и БД
sudo -u postgres psql -c "CREATE USER ipwhitelist WITH PASSWORD '${PASSWORD}';"
sudo -u postgres psql -c "CREATE DATABASE ipwhitelist OWNER ipwhitelist;"
# Применить схему
PGPASSWORD="${PASSWORD}" psql -h localhost -U ipwhitelist -d ipwhitelist -f /opt/ipwhitelist/sql/schema.sql
# Настройка: слушать только localhost (безопасность)
grep -c "^listen_addresses = 'localhost'" /etc/postgresql/16/main/postgresql.conf \
|| echo "listen_addresses = 'localhost'" >> /etc/postgresql/16/main/postgresql.conf
systemctl restart postgresql
```
---
## 3. .env (PRODUCTION)
```bash
cp /opt/ipwhitelist/.env.example /opt/ipwhitelist/.env
chmod 600 /opt/ipwhitelist/.env
```
```ini
# ── База данных ──
DB_HOST=localhost
DB_PORT=5432
DB_NAME=ipwhitelist
DB_USER=super
DB_PASS=ваш_пароль
DB_SSLMODE=disable
DB_USER=ipwhitelist
DB_PASS=<сгенерировать: openssl rand -base64 24>
DB_SSLMODE=disable # localhost — шифрование не нужно
# ── Режим разработки (оставить true пока нет Keycloak) ──
DEV_MODE=true
# ── Аутентификация ──
DEV_MODE=false # ОБЯЗАТЕЛЬНО false в production
ADMIN_CLIENT_ID=WZ01112 # clientId пользователя-администратора
# ── Keycloak / OIDC (данные от DevOps) ──
KC_CLIENT_ID=<client_id> # зарегистрированный OIDC-клиент
KC_CLIENT_SECRET=<secret> # секрет клиента
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
APP_URL=https://<домен> # публичный URL приложения (без слеша)
# ── Сессия ──
SESSION_SECRET=<openssl rand -base64 32>
CSRF_SECRET=<openssl rand -base64 32>
# ── Лимит записей по умолчанию ──
DEFAULT_LIMIT=15
# ── Сессия (сменить на случайные строки) ──
SESSION_SECRET=случайная_строка_32_символа
CSRF_SECRET=другая_случайная_строка_32_символа
# ── Порт ──
# ── Порт (для nginx upstream) ──
PORT=3001
# ── Админ (clientId администратора) ──
ADMIN_CLIENT_ID=WZ01112
# ── Keycloak (не заполнять пока нет) ──
# KC_CLIENT_ID=ipwhitelist
# KC_CLIENT_SECRET=секрет
# KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
# APP_URL=https://ваш-домен.ru
# ── API_BASE — куда UI-слой ходит за API (обычно localhost) ──
API_BASE=http://localhost:3001
```
## nginx
**Генерация секретов:**
```bash
openssl rand -base64 32 # для SESSION_SECRET
openssl rand -base64 32 # для CSRF_SECRET
openssl rand -base64 24 # для DB_PASS
```
---
## 4. Keycloak — регистрация OIDC-клиента
**Что сделать в админке Keycloak (realm `cloud`):**
1. Создать клиента:
- Client ID: `<домен>` (например `white.nubes.ru`)
- Client authentication: ON
- Standard flow: ON
- Valid redirect URIs: `https://<домен>/callback`
- Web origins: `https://<домен>` (или `+`)
2. Забрать `Client secret` → в .env как `KC_CLIENT_SECRET`
3. **Mapper'ы** (создать на клиенте, тип `User Attribute → Token Claim`):
| Claim name | User attribute | Назначение |
|---|---|---|
| `ClientID` | `client_id` | ID компании (может быть через запятую) |
| `company_id` | `company_id` | ID компании в БД |
| `company_name` | `company_name` | Название компании |
4. **Пользователи** — создать в Keycloak с заполненными атрибутами `client_id`, `company_id`, `company_name`.
**Формат multi-company:** `client_id = "WZ11125,WZ03816"` — первое значение до запятой = активная компания. Приложение парсит автоматически.
---
## 5. nginx + HTTPS
```nginx
server {
listen 80;
server_name ваш-домен.ru;
server_name <домен>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name <домен>;
ssl_certificate /etc/letsencrypt/live/<домен>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<домен>/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
location / {
proxy_pass http://127.0.0.1:3001;
@@ -75,27 +142,109 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
```
## Keycloak (когда появится)
```bash
# Получить сертификат
certbot --nginx -d <домен>
Нужно будет вписать в `.env`:
```
DEV_MODE=false
KC_CLIENT_ID=ipwhitelist
KC_CLIENT_SECRET=...
KC_BASE_URL=https://keycloak.nubes.ru/realms/cloud
APP_URL=https://ваш-домен.ru
# Проверить авто-обновление
certbot renew --dry-run
```
И в админке Keycloak зарегистрировать клиента (см. `docs/deploy-keycloak.md`).
---
## Проверка
## 6. PM2 — автозапуск
| URL | Что должно быть |
```bash
npm i -g pm2
# Запуск
pm2 start /opt/ipwhitelist/server.js --name ipwhitelist --update-env
# Автостарт при перезагрузке
pm2 startup systemd
pm2 save
# Проверка
pm2 status
pm2 logs ipwhitelist --lines 5
# Должно быть: [auth] OIDC — Keycloak
# Если [auth] MOCK — значит KC_CLIENT_ID не задан
```
---
## 7. Порядок ввода в эксплуатацию
```bash
# 1. Убедиться что DEV_MODE=false и заданы KC_*
grep -E "DEV_MODE|KC_CLIENT_ID|KC_CLIENT_SECRET" /opt/ipwhitelist/.env
# 2. Перезапустить
pm2 restart ipwhitelist --update-env
# 3. Проверить логи — должен быть OIDC-режим
pm2 logs ipwhitelist --lines 3 --nostream
# 4. Проверить health
curl -s https://<домен>/healthz
# → OK
# 5. Проверить SSO: открыть https://<домен>/login в браузере
# → редирект на Keycloak
# → после входа → редирект обратно на главную
```
---
## 8. Проверка
| URL | Ожидаемый результат |
|---|---|
| `https://ваш-домен.ru/healthz` | `OK` |
| `https://ваш-домен.ru/export` | Список CIDR (text/plain) |
| `https://ваш-домен.ru/` | Страница входа (при DEV_MODE) |
| `https://<домен>/healthz` | `OK` |
| `https://<домен>/` | Редирект на Keycloak login (если нет сессии) |
| `https://<домен>/login` | Редирект на Keycloak (если `DEV_MODE=false`) |
| `https://<домен>/export` | CIDR-список (text/plain) |
| `https://<домен>/api/v1/entries` | 401 (нужен Bearer) |
| `https://<домен>/callback?code=...` | Обмен code → сессия → редирект на `/` |
---
## 9. Обновление
```bash
cd /opt/ipwhitelist
git pull
npm install --production
pm2 restart ipwhitelist --update-env
```
---
## 10. Переменные окружения (справочно)
| Переменная | Обязательна | По умолчанию | Описание |
|---|---|---|---|
| `DB_HOST` | да | — | Хост PostgreSQL |
| `DB_PORT` | нет | 5432 | Порт PostgreSQL |
| `DB_NAME` | да | — | Имя БД |
| `DB_USER` | да | — | Пользователь БД |
| `DB_PASS` | да | — | Пароль БД |
| `DB_SSLMODE` | нет | disable | `require` для внешнего подключения |
| `DEV_MODE` | нет | true | `false` в production (ОБЯЗАТЕЛЬНО) |
| `ADMIN_CLIENT_ID` | нет | WZ01112 | clientId администратора |
| `KC_CLIENT_ID` | да* | — | OIDC client ID (обязателен для SSO) |
| `KC_CLIENT_SECRET` | да* | — | OIDC client secret |
| `KC_BASE_URL` | нет | `https://keycloak.nubes.ru/realms/cloud` | Keycloak realm URL |
| `APP_URL` | да* | — | Публичный URL приложения (для OIDC redirect) |
| `SESSION_SECRET` | да | — | Подпись сессионных cookie |
| `CSRF_SECRET` | нет | — | CSRF-токены |
| `PORT` | нет | 3000 | Порт Express |
| `DEFAULT_LIMIT` | нет | 15 | Лимит записей по умолчанию |
| `API_BASE` | нет | `http://localhost:PORT` | URL для UI→API запросов |
\* — обязательны при `DEV_MODE=false`
+38 -13
View File
@@ -66,16 +66,17 @@ async function initAuth() {
const isOidc = !!(KC_CLIENT_ID && KC_CLIENT_SECRET);
if (isOidc) {
console.log('[auth] OIDC — Keycloak', KC_BASE_URL);
// Предзагрузка JWKS при старте — избегаем задержки на первом запросе.
// При ошибке логируем предупреждение: JWKS будет загружен в exchangeCode().
// Всегда предзагружаем JWKS (даже в mock-режиме), чтобы можно было
// проверить реальный Bearer токен от Keycloak без KC_CLIENT_ID/SECRET.
try {
cachedJwks = JSON.parse(await httpGet(OIDC.certs));
console.log('[auth] JWKS loaded:', cachedJwks.keys.length, 'ключей');
} catch (e) {
console.warn('[auth] JWKS preload failed:', e.message, '— будет загружен при первом запросе');
console.warn('[auth] JWKS preload failed:', e.message, '— только mock-режим');
}
if (isOidc) {
console.log('[auth] OIDC — Keycloak', KC_BASE_URL);
} else {
console.log('[auth] MOCK — local RS256 key');
mockKeyPair = crypto.generateKeyPairSync('rsa', {
@@ -122,7 +123,7 @@ function createMiddleware(isOidc) {
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
if (bearer) {
try {
const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
const payload = verifyAnyToken(bearer);
req.user = userFromPayload(payload);
return next();
} catch (e) {
@@ -234,12 +235,36 @@ function verifyOidcToken(token) {
if (!key) throw new Error('No matching RSA sig key in JWKS' + (kid ? ` (kid=${kid})` : ''));
const pem = crypto.createPublicKey({ key, format: 'jwk' }).export({ type: 'spki', format: 'pem' });
// Проверяем issuer и audience — защита от токенов других realm/клиентов (A02).
return jwt.verify(token, pem, {
algorithms: ['RS256'],
issuer: KC_BASE_URL,
audience: KC_CLIENT_ID,
});
// Проверяем issuer — защита от токенов других realm.
// audience проверяем только если задан KC_CLIENT_ID (для OIDC-режима).
const opts = { algorithms: ['RS256'], issuer: KC_BASE_URL };
if (KC_CLIENT_ID) opts.audience = KC_CLIENT_ID;
return jwt.verify(token, pem, opts);
}
/**
* Универсальная верификация: пробует mock, затем OIDC (JWKS).
* Позволяет принимать и mock-токены (тесты, dev), и реальные токены от Keycloak.
*/
function verifyAnyToken(token) {
let lastErr = null;
// 1. Mock (быстрый, локальный)
if (mockKeyPair) {
try {
return jwt.verify(token, mockKeyPair.publicKey, { algorithms: ['RS256'], issuer: ISSUER });
} catch (e) {
lastErr = e;
}
}
// 2. OIDC (JWKS от Keycloak)
if (cachedJwks) {
try {
return verifyOidcToken(token);
} catch (e) {
lastErr = e;
}
}
throw lastErr || new Error('Token verification failed: no verifier available');
}
function userFromPayload(payload) {
@@ -269,7 +294,7 @@ function createBearerMiddleware(isOidc) {
return res.status(401).json({ error: 'Bearer token required' });
}
try {
const payload = isOidc ? verifyOidcToken(bearer) : verifyMockToken(bearer);
const payload = verifyAnyToken(bearer);
req.user = userFromPayload(payload);
return next();
} catch (e) {
+42 -4
View File
@@ -67,23 +67,61 @@ function createRouter({ auth, MOCK_USERS, authLimiter }) {
// GET /login-token — редирект на /login (форма вставки токена там же)
router.get('/login-token', (req, res) => res.redirect('/login'));
// POST /login-token — ручной вход по clientId (отладка)
// POST /login-token — вход по Bearer JWT токену (реальному) или clientId (отладка)
router.post('/login-token', authLimiter, (req, res) => {
const { clientId, returnTo } = req.body;
const { token, clientId, returnTo } = req.body;
// ── Вход по реальному JWT токену ──────────────────────────────────────
if (token && token.trim()) {
const raw = token.trim().replace(/^Bearer\s+/i, '');
// Минимальная проверка: это JWT (3 части, разделённые точками)
const parts = raw.split('.');
if (parts.length !== 3) {
return res.redirect('/login?error=Невалидный+JWT+токен');
}
// Декодируем payload (без проверки подписи — API проверит)
let payload;
try {
const buf = Buffer.from(parts[1], 'base64url');
payload = JSON.parse(buf.toString());
if (!payload || typeof payload !== 'object') throw new Error('empty');
} catch (_) {
return res.redirect('/login?error=Невалидный+payload+токена');
}
// Извлекаем данные пользователя из payload
const rawClientId = payload.ClientID || payload.client_id || payload.preferred_username || payload.sub || '';
const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
const activeClientId = allClientIds[0] || rawClientId;
req.session.token = raw;
req.session.user = {
clientId: rawClientId,
allClientIds,
activeClientId,
email: payload.email || payload.login || activeClientId + '@unknown',
companyId: payload.company_id || payload.companyId || null,
companyName: payload.company_name || payload.companyName || activeClientId,
isAdmin: activeClientId === (process.env.ADMIN_CLIENT_ID || 'WZ01112'),
};
return res.redirect(returnTo || '/');
}
// ── Вход по clientId (отладка, mock) ───────────────────────────────────
if (!clientId || !clientId.trim()) return res.redirect('/login?error=Введите+clientId');
const trimmed = clientId.trim();
const allClientIds = trimmed.split(',').map(s => s.trim()).filter(Boolean);
const activeClientId = allClientIds[0];
const token = auth.issueMockToken({
const mockToken = auth.issueMockToken({
clientId: trimmed,
companyId: '00000000-0000-0000-0000-000000000001',
companyName: activeClientId,
email: activeClientId + '@debug.local',
});
req.session.token = token;
req.session.token = mockToken;
req.session.user = {
clientId: trimmed,
allClientIds,
+13
View File
@@ -84,6 +84,19 @@
<hr>
<% } %>
<%# ── Блок ручного ввода JWT токена ── %>
<p class="section-title">Вход по Bearer токену</p>
<form method="POST" action="/login-token">
<input type="hidden" name="returnTo" value="<%= returnTo || '/' %>">
<div class="field">
<label>JWT токен (вставьте полный Bearer токен)</label>
<textarea name="token" placeholder="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." rows="3" autocomplete="off"></textarea>
</div>
<button class="btn" type="submit">Войти по токену</button>
</form>
<hr>
<%# ── Блок ручного ввода clientId (отладка) ── %>
<p class="section-title">Ручной вход (clientId)</p>
<form method="POST" action="/login-token">