export type DocBlock = | { type: 'paragraph'; text: string } | { type: 'list'; items: string[] } | { type: 'code'; language: string; code: string; title?: string } | { type: 'callout'; variant: 'info' | 'warning' | 'tip'; title: string; text: string } | { type: 'table'; headers: string[]; rows: string[][] } | { type: 'oauth-examples' } | { type: 'auth-login-examples' } | { type: 'auth-ldap-examples' } | { type: 'api-reference' }; export interface DocSection { id: string; title: string; blocks: DocBlock[]; } export interface DocPage { slug: string; title: string; description: string; sections: DocSection[]; } export const docPages: DocPage[] = [ { slug: 'getting-started', title: 'Начало работы', description: 'Обзор экосистемы Identity Provider, порты сервисов и быстрый локальный запуск.', sections: [ { id: 'overview', title: 'Обзор', blocks: [ { type: 'paragraph', text: 'Lendry ID — enterprise Identity Provider в стиле Yandex ID: единый аккаунт, OAuth 2.0, LDAP/LDAPS, семейные группы, чат, PIN-блокировка сессий и админ-панель. Монорепозиторий состоит из NestJS-микросервисов, Go-сервисов и Next.js-приложений.' }, { type: 'callout', variant: 'info', title: 'Домен', text: 'Production-домен задаётся в админке (PROJECT_DOMAIN и PUBLIC_API_URL). Локально API — порт 3000, frontend — 3002, документация — 3003. При same-origin деплое PUBLIC_API_URL обычно https://ваш-домен/idp-api.' } ] }, { id: 'requirements', title: 'Требования', blocks: [ { type: 'list', items: [ 'Node.js 20.19+ и npm 10+', 'Docker и Docker Compose', 'PostgreSQL 16, Redis 7, RabbitMQ 3.13, MinIO', 'Go 1.23+ (для локальной сборки media-ws и ldap-auth)' ] } ] }, { id: 'quick-start', title: 'Быстрый запуск', blocks: [ { type: 'callout', variant: 'tip', title: 'Production на сервере', text: 'Для развёртывания на Linux с Docker, Nginx и SSL используйте скрипт install.sh — подробности в разделе «Развёртывание на сервере».' }, { type: 'code', language: 'bash', title: 'Сервер (рекомендуется)', code: `git clone https://git.lendry.ru/lendry/IdP.git cd IdP chmod +x install.sh ./install.sh` }, { type: 'code', language: 'bash', title: 'Локально через Docker', code: `git clone lendry-id cd lendry-id docker compose up -d --build # Проверка curl http://localhost:3000/health curl http://localhost:3003/docs/getting-started` }, { type: 'table', headers: ['Сервис', 'Порт', 'Назначение'], rows: [ ['api-gateway', '3000', 'REST API, Swagger'], ['sso-core', '3001 / 50051', 'Бизнес-логика, gRPC'], ['frontend', '3002', 'UI входа и профиля'], ['docs', '3003', 'Документация'], ['media-ws', '8085', 'WebSocket уведомлений и чата'], ['ldap-auth', '8086', 'LDAP/LDAPS аутентификация'], ['MinIO', '9000 / 9001', 'Хранилище медиа'], ['RabbitMQ', '5672 / 15672', 'Очереди событий'] ] } ] }, { id: 'first-user', title: 'Первый пользователь', blocks: [ { type: 'paragraph', text: 'Первый зарегистрированный пользователь автоматически получает права супер-администратора (isSuperAdmin). Race condition защищена Redis-lock и Serializable-транзакцией PostgreSQL.' }, { type: 'code', language: 'bash', title: 'Регистрация через API', code: `curl -X POST http://localhost:3000/auth/register \\ -H "Content-Type: application/json" \\ -d '{"displayName":"Администратор","email":"admin@example.com","password":"SecurePass123"}'` } ] } ] }, { slug: 'architecture', title: 'Архитектура', description: 'Схема микросервисов, потоки данных и технологический стек.', sections: [ { id: 'stack', title: 'Технологический стек', blocks: [ { type: 'table', headers: ['Компонент', 'Технологии'], rows: [ ['sso-core', 'NestJS, Prisma 7+, PostgreSQL, gRPC'], ['api-gateway', 'NestJS, REST, Swagger'], ['frontend / docs', 'Next.js App Router, React, Tailwind, shadcn/ui'], ['media-ws', 'Go, Gorilla WebSocket, Redis, RabbitMQ'], ['ldap-auth', 'Go, go-ldap'], ['Инфраструктура', 'Docker, Redis, RabbitMQ, MinIO'] ] } ] }, { id: 'flow', title: 'Поток запросов', blocks: [ { type: 'paragraph', text: 'Клиент (браузер или стороннее приложение) обращается к api-gateway по REST. Gateway проксирует вызовы в sso-core через gRPC. Медиафайлы загружаются в MinIO через presigned URL. Realtime-события (уведомления, чат) доставляются через media-ws по WebSocket с JWT-авторизацией.' }, { type: 'list', items: [ 'JWT access + refresh tokens для сессий', 'PIN-код: при включении сессия создаётся с pinVerified=false', 'SystemSetting — динамические бизнес-правила без хардкода во frontend', 'LinkedAccount — привязка LDAP, Google, Yandex и других провайдеров' ] } ] } ] }, { slug: 'deployment', title: 'Развёртывание на сервере', description: 'Автоматическая установка через install.sh: Docker, Nginx, SSL и отдельные домены для каждого сервиса.', sections: [ { id: 'install-script', title: '1. Автоматическая установка (install.sh)', blocks: [ { type: 'paragraph', text: 'Скрипт install.sh разворачивает весь стек одной командой: Docker, Nginx, генерация .env, reverse proxy. Поддерживает локальную сеть без интернета (--intranet), самоподписанный HTTPS и Let\'s Encrypt для публичных серверов. На Windows Server Nginx запускается в Docker-контейнере.' }, { type: 'callout', variant: 'tip', title: 'Локальная сеть (AD, .lpr, .local)', text: 'Если домены только внутри LAN и Let\'s Encrypt недоступен — используйте ./install.sh --intranet --ssl none. Доступ: http://api.idpmvk.lpr без выхода в интернет.' }, { type: 'code', language: 'bash', title: 'Быстрый старт', code: `git clone https://git.lendry.ru/lendry/IdP.git cd IdP chmod +x install.sh ./install.sh` }, { type: 'code', language: 'bash', title: 'Production без вопросов', code: `./install.sh --install --yes \\ --api-domain api.id.example.ru \\ --frontend-domain id.example.ru \\ --docs-domain docs.example.ru \\ --email admin@example.ru` }, { type: 'code', language: 'bash', title: 'Установка без клонирования', code: `curl -fsSL https://git.lendry.ru/lendry/IdP/raw/branch/main/install.sh | bash` } ] }, { id: 'intranet-network', title: '2. Локальная сеть (без интернета)', blocks: [ { type: 'paragraph', text: 'Для корпоративной LAN, Windows Server и внутренних доменов (например idpmvk.lpr) не нужен доступ из интернета и Let\'s Encrypt. Скрипт настраивает Nginx на HTTP или HTTPS с самоподписанным сертификатом (openssl). На Windows Server Nginx работает в Docker (профиль proxy).' }, { type: 'code', language: 'bash', title: 'Рекомендуемая команда (HTTP)', code: `./install.sh --intranet --install --yes --offline \\ --api-domain api.idpmvk.lpr \\ --frontend-domain id.idpmvk.lpr \\ --docs-domain docs.idpmvk.lpr \\ --ssl none` }, { type: 'code', language: 'bash', title: 'HTTPS самоподписанный (опционально)', code: `./install.sh --intranet --install --yes --offline \\ --api-domain api.idpmvk.lpr \\ --frontend-domain id.idpmvk.lpr \\ --docs-domain docs.idpmvk.lpr \\ --ssl selfsigned` }, { type: 'table', headers: ['SSL_TYPE', 'Описание', 'Интернет'], rows: [ ['none', 'http://api.idpmvk.lpr — без сертификатов', 'Не нужен'], ['selfsigned', 'https://... — openssl, предупреждение браузера', 'Не нужен'], ['letsencrypt', 'Публичный HTTPS', 'Нужен'] ] }, { type: 'callout', variant: 'warning', title: 'DNS на клиентах', text: 'Пропишите A-записи во внутреннем DNS Active Directory или файл hosts на каждом ПК: IP-сервера → api.idpmvk.lpr, id.idpmvk.lpr, docs.idpmvk.lpr. Windows: C:\\Windows\\System32\\drivers\\etc\\hosts' }, { type: 'code', language: 'text', title: 'Пример hosts (Windows / Linux)', code: `192.168.1.10 api.idpmvk.lpr 192.168.1.10 id.idpmvk.lpr 192.168.1.10 docs.idpmvk.lpr` }, { type: 'callout', variant: 'info', title: 'Windows Server', text: 'Запускайте install.sh из Git Bash или WSL. Docker Desktop / Docker Engine должен быть установлен заранее (--offline). Nginx поднимается контейнером на портах 80 и 443.' } ] }, { id: 'server-requirements', title: '3. Требования к серверу', blocks: [ { type: 'list', items: [ 'Ubuntu 22.04+ / Debian 12+ / Fedora / RHEL (Linux)', 'Минимум 4 GB RAM, 2 vCPU, 40 GB SSD', 'Отдельные домены или поддомены для API, Frontend и Docs (A-запись → IP сервера)', 'Открытые порты 80 и 443 (остальные сервисы доступны только через Nginx на localhost)', 'Права sudo/root для установки Docker и Nginx' ] } ] }, { id: 'domains', title: '4. Отдельные домены для сервисов', blocks: [ { type: 'paragraph', text: 'При установке и при повторной настройке (--configure-domains) можно указать свой домен для каждого компонента. Текущие значения подставляются по умолчанию — достаточно изменить только нужные.' }, { type: 'table', headers: ['Сервис', 'Параметр', 'Пример', 'Nginx → порт'], rows: [ ['API / Swagger', '--api-domain', 'api.id.example.ru', '127.0.0.1:3000'], ['Frontend (UI)', '--frontend-domain', 'id.example.ru', '127.0.0.1:3002'], ['Документация', '--docs-domain', 'docs.example.ru', '127.0.0.1:3003'], ['WebSocket', '--ws-domain', 'пусто = API + /ws', '127.0.0.1:8085'], ['MinIO S3', '--minio-domain', 'опционально', '127.0.0.1:9000'], ['MinIO Console', '--minio-console-domain', 'опционально', '127.0.0.1:9001'] ] }, { type: 'callout', variant: 'info', title: 'WebSocket', text: 'Если --ws-domain не указан, WebSocket проксируется на том же домене, что и API, по пути /ws (wss://api.example.ru/ws). Для отдельного домена укажите --ws-domain ws.example.ru.' } ] }, { id: 'install-menu', title: '5. Меню управления (повторный запуск)', blocks: [ { type: 'paragraph', text: 'Если файл .env уже существует, повторный вызов ./install.sh открывает интерактивное меню — как у панелей с автоматическим reverse proxy.' }, { type: 'table', headers: ['Пункт', 'Действие'], rows: [ ['1', 'Полная установка / переустановка стека'], ['2', 'Настроить или изменить домены, Nginx и SSL'], ['3', 'Обновить SSL-сертификаты (Let\'s Encrypt)'], ['4', 'Перезапуск / пересборка контейнеров'], ['5', 'Статус сервисов и проверка Nginx'], ['6', 'Удалить конфиги Nginx IdP'], ['0', 'Выход'] ] }, { type: 'code', language: 'bash', title: 'Смена доменов после установки', code: `# Интерактивно ./install.sh --configure-domains # Или через меню ./install.sh # → пункт 2 # Пример: вынести Swagger на отдельный домен ./install.sh --configure-domains \\ --api-domain swagger.id.example.ru \\ --frontend-domain id.example.ru \\ --docs-domain docs.example.ru` } ] }, { id: 'install-commands', title: '6. Команды и флаги', blocks: [ { type: 'table', headers: ['Команда / флаг', 'Описание'], rows: [ ['./install.sh', 'Интерактивное меню (если .env есть) или первая установка'], ['./install.sh --install', 'Полная установка'], ['./install.sh --configure-domains', 'Изменить домены и пересобрать frontend/docs'], ['./install.sh --renew-ssl', 'Обновить сертификаты Let\'s Encrypt'], ['./install.sh --restart', 'docker compose up -d --build'], ['./install.sh --status', 'Статус контейнеров и доменов'], ['./install.sh --intranet', 'Локальная сеть, HTTP по умолчанию'], ['./install.sh --ssl none', 'Только HTTP через Nginx'], ['./install.sh --ssl selfsigned', 'HTTPS самоподписанный'], ['./install.sh --offline', 'Не скачивать Docker из интернета'], ['./install.sh --nginx-mode docker', 'Nginx в контейнере (Windows)'], ['./install.sh --local', 'localhost:3000/3002/3003 без Nginx'], ['./install.sh --yes', 'Без интерактивных вопросов'], ['FORCE_ENV=1 ./install.sh', 'Пересоздать .env с новыми секретами'] ] }, { type: 'code', language: 'bash', title: 'Локальная разработка без доменов', code: `./install.sh --local # или ./install.sh --install --local --yes` } ] }, { id: 'install-automation', title: '7. Что делает скрипт автоматически', blocks: [ { type: 'list', items: [ 'Устанавливает curl, git, openssl, jq (apt/dnf)', 'Устанавливает Docker и Docker Compose v2 (get.docker.com)', 'Добавляет пользователя в группу docker', 'Генерирует .env: пароли PostgreSQL, RabbitMQ, MinIO, JWT-секреты, DATA_ENCRYPTION_KEY', 'Создаёт docker-compose.override.yml — привязка портов к 127.0.0.1', 'Устанавливает Nginx и Certbot, настраивает UFW (80, 443)', 'Генерирует конфиги /etc/nginx/sites-available/lendry-id-*.conf', 'Получает SSL через certbot (webroot), включает certbot.timer', 'Запускает docker compose up -d --build и ждёт health сервисов', 'При смене доменов пересобирает frontend и docs с новыми NEXT_PUBLIC_* URL' ] }, { type: 'callout', variant: 'warning', title: 'Секреты', text: 'Файл .env не коммитьте в git. Сохраните его в надёжном месте после первой установки. FORCE_ENV=1 генерирует новые секреты и сбрасывает старые.' } ] }, { id: 'clone-config', title: '8. Переменные .env (генерируются автоматически)', blocks: [ { type: 'paragraph', text: 'install.sh создаёт и обновляет .env на основе .env.example. При ручном редактировании не меняйте секреты без необходимости — для смены доменов используйте ./install.sh --configure-domains.' }, { type: 'code', language: 'bash', title: 'Ключевые переменные .env', code: `# Домены (без протокола) DOMAIN_API=api.id.example.ru DOMAIN_FRONTEND=id.example.ru DOMAIN_DOCS=docs.example.ru DOMAIN_WS= # Публичные URL (скрипт подставляет https://) PUBLIC_API_URL=https://api.id.example.ru PUBLIC_FRONTEND_URL=https://id.example.ru PUBLIC_DOCS_URL=https://docs.example.ru PUBLIC_WS_URL=wss://api.id.example.ru/ws USE_NGINX_SSL=true CERTBOT_EMAIL=admin@example.ru INSTALL_MODE=production # Секреты — генерируются install.sh JWT_ACCESS_SECRET=... JWT_REFRESH_SECRET=... DATA_ENCRYPTION_KEY=... POSTGRES_PASSWORD=... RABBITMQ_DEFAULT_PASS=... MINIO_ROOT_PASSWORD=...` } ] }, { id: 'env-services', title: '9. Переменные окружения сервисов', blocks: [ { type: 'table', headers: ['Сервис', 'Ключевые переменные'], rows: [ ['sso-core', 'DATABASE_URL, JWT_*_SECRET, DATA_ENCRYPTION_KEY, REDIS_URL, RABBITMQ_URL, MINIO_*, LDAP_AUTH_URL, PUBLIC_API_URL'], ['api-gateway', 'SSO_CORE_GRPC_URL, JWT_ACCESS_SECRET, PUBLIC_API_URL, MINIO_*'], ['frontend', 'NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WS_URL (build args + runtime)'], ['docs', 'NEXT_PUBLIC_API_URL, INTERNAL_API_URL'], ['media-ws', 'REDIS_ADDR, RABBITMQ_URL, JWT_ACCESS_SECRET'], ['ldap-auth', 'PORT=8086'] ] } ] }, { id: 'after-install', title: '10. После установки', blocks: [ { type: 'table', headers: ['Ресурс', 'URL (production)'], rows: [ ['API / Swagger', 'https:///api'], ['Frontend', 'https://'], ['Документация', 'https:///docs'], ['WebSocket', 'wss:///ws или wss:///ws'], ['MinIO Console', 'только localhost:9001 (если не задан домен)'], ['RabbitMQ UI', 'только localhost:15672'] ] }, { type: 'callout', variant: 'tip', title: 'Первый пользователь', text: 'Первый зарегистрированный пользователь автоматически становится супер-администратором. Название проекта (PROJECT_NAME) и LDAP настраиваются в админке: /admin/settings.' }, { type: 'code', language: 'bash', title: 'Полезные команды', code: `docker compose ps docker compose logs -f api-gateway sso-core ./install.sh --status ./install.sh --renew-ssl` } ] }, { id: 'migrations', title: '11. Миграции и seed', blocks: [ { type: 'code', language: 'bash', code: `# Prisma migrate внутри контейнера sso-core docker compose exec sso-core npx prisma migrate deploy # Seed системных настроек (PROJECT_NAME, LDAP_*, PIN_*) docker compose restart sso-core` }, { type: 'callout', variant: 'info', title: 'SystemSetting', text: 'Название проекта (PROJECT_NAME), tagline и LDAP-параметры настраиваются в админке. Frontend и docs подтягивают PROJECT_NAME через GET /settings/public.' } ] }, { id: 'backup', title: '12. Резервное копирование', blocks: [ { type: 'list', items: [ 'PostgreSQL: pg_dump lendry_id ежедневно', 'MinIO: mc mirror или snapshot volume minio_data', 'Redis: RDB/AOF snapshots при необходимости сохранения сессий', 'Секреты .env — хранить в vault, не в репозитории' ] }, { type: 'code', language: 'bash', code: `docker compose exec postgres pg_dump -U lendry lendry_id > backup-$(date +%F).sql` } ] }, { id: 'monitoring', title: '13. Мониторинг и обновления', blocks: [ { type: 'list', items: [ 'Health: GET /health на api-gateway и sso-core', 'Логи: docker compose logs -f --tail=200', 'Обновление кода: git pull && ./install.sh --restart', 'Смена доменов: ./install.sh --configure-domains', 'SSL: ./install.sh --renew-ssl или автоматически через certbot.timer' ] } ] } ] }, { slug: 'authentication', title: 'Аутентификация', description: 'Способы входа: TOTP (Google Authenticator), OTP, пароль, LDAP, PIN и refresh-сессии.', sections: [ { id: 'flows', title: 'Сценарии входа', blocks: [ { type: 'table', headers: ['Сценарий', 'Endpoint', 'Описание'], rows: [ ['Identifier-first', 'POST /auth/identify', 'Проверка пользователя, isTotpEnabled, otpChannels и альтернативных способов'], ['TOTP (основной)', 'POST /auth/totp/begin + verify', 'Вход через Google Authenticator, если аутентификатор подключён'], ['Passwordless OTP (альтернатива)', 'POST /auth/otp/send + verify', '6-значный код на почту/телефон вместо TOTP'], ['Пароль', 'POST /auth/login/password', 'Альтернативный вход по паролю'], ['LDAP/LDAPS', 'POST /auth/ldap/login', 'Корпоративный вход (требует LDAP_ENABLED)'], ['PIN', 'POST /auth/pin/verify', 'Разблокировка сессии после входа с PIN'] ] } ] }, { id: 'totp-login', title: 'Вход через приложение-аутентификатор (TOTP)', blocks: [ { type: 'paragraph', text: 'Если пользователь подключил Google Authenticator (или аналог) в разделе «Безопасность», код из приложения становится основным способом входа вместо SMS/email OTP. SMS и email используются только как альтернатива через «Другой способ входа».' }, { type: 'list', items: [ '1. POST /auth/identify с почтой или телефоном — в ответе isTotpEnabled: true и otpChannels (доступные каналы для альтернативного OTP).', '2. POST /auth/totp/begin — создаёт challenge (totpChallengeToken, TTL 5 минут). SMS/email на этом шаге не отправляются.', '3. Пользователь вводит 6-значный код из приложения-аутентификатора.', '4. POST /auth/totp/verify с totpChallengeToken и code — выдаётся JWT и refresh token (далее при необходимости PIN).' ] }, { type: 'callout', variant: 'tip', title: 'TOTP и SMS/email — альтернативы', text: 'Код из аутентификатора и код из SMS/email не комбинируются: это два разных способа пройти один и тот же шаг входа. После успешного OTP verify TOTP повторно не запрашивается.' }, { type: 'table', headers: ['Ситуация', 'Поведение UI / API'], rows: [ ['Аутентификатор подключён', 'После identify сразу экран TOTP (totp/begin), без автоматической отправки SMS/email'], ['Альтернатива: код на почту/телефон', 'POST /auth/otp/send с channel: email или phone, затем POST /auth/otp/verify'], ['Привязаны и почта, и телефон', 'Пользователь выбирает канал (otpChannels) перед отправкой OTP'], ['Только почта или только телефон', 'OTP отправляется сразу на единственный доступный канал'], ['Альтернатива: пароль', 'POST /auth/login/password — без дополнительного TOTP'], ['Включён PIN', 'После TOTP или OTP — POST /auth/pin/verify для полной сессии'] ] }, { type: 'code', title: 'POST /auth/identify — фрагмент ответа', language: 'json', code: `{ "exists": true, "hasPassword": true, "isPinEnabled": false, "isTotpEnabled": true, "otpChannels": [ { "channel": "email", "masked": "u***@example.com" }, { "channel": "phone", "masked": "+7********42" } ], "methods": [ { "kind": "password", "channel": "password", "masked": "Пароль" } ] }` }, { type: 'code', title: 'Начать вход по TOTP', language: 'bash', code: `curl -X POST http://localhost:3000/auth/totp/begin \\ -H "Content-Type: application/json" \\ -d '{ "recipient": "user@example.com", "fingerprint": "device-fingerprint-uuid", "deviceName": "Chrome на Windows", "deviceType": "WEB" }' # Ответ: { "totpChallengeToken": "eyJ..." }` }, { type: 'code', title: 'Подтвердить код аутентификатора', language: 'bash', code: `curl -X POST http://localhost:3000/auth/totp/verify \\ -H "Content-Type: application/json" \\ -d '{ "totpChallengeToken": "eyJ...", "code": "123456" }' # Ответ: AuthTokens (accessToken, refreshToken, sessionId, pinVerified, user)` }, { type: 'code', title: 'Альтернатива: OTP на выбранный канал', language: 'bash', code: `# Отправить код (channel: email | phone | backupEmail | backupPhone) curl -X POST http://localhost:3000/auth/otp/send \\ -H "Content-Type: application/json" \\ -d '{ "recipient": "user@example.com", "channel": "phone" }' # Подтвердить OTP — сразу выдаёт сессию, без TOTP curl -X POST http://localhost:3000/auth/otp/verify \\ -H "Content-Type: application/json" \\ -d '{ "recipient": "user@example.com", "code": "654321", "fingerprint": "device-fingerprint-uuid", "deviceName": "Chrome на Windows", "deviceType": "WEB" }'` } ] }, { id: 'totp-setup', title: 'Подключение аутентификатора', blocks: [ { type: 'paragraph', text: 'Настройка выполняется в личном кабинете: Безопасность → Дополнительная защита → Подключить аутентификатор. Название в Google Authenticator берётся из SystemSetting PROJECT_NAME (не захардкожено).' }, { type: 'list', items: [ 'GET /security/users/{userId}/totp/status — проверка, включён ли TOTP', 'POST /security/users/{userId}/totp/setup — QR-код (otpauthUrl) и секретный ключ', 'POST /security/users/{userId}/totp/enable — подтверждение первого кода и активация', 'POST /security/users/{userId}/totp/disable — отключение с проверкой текущего кода' ] }, { type: 'callout', variant: 'info', title: 'Повторная настройка', text: 'Если настройка начата, но не завершена, повторный вызов setup возвращает тот же секрет — не нужно сканировать новый QR, пока не истёк незавершённый challenge.' } ] }, { id: 'login-example', title: 'Примеры входа по паролю', blocks: [ { type: 'paragraph', text: 'Endpoint POST /auth/login/password принимает login (почта, телефон или username), password и метаданные устройства. Ниже — готовые примеры на разных языках.' }, { type: 'auth-login-examples' }, { type: 'callout', variant: 'info', title: 'PIN-блокировка', text: 'Если у пользователя включён PIN, ответ содержит requiresPin=true и ограниченную сессию. Полный JWT выдаётся после POST /auth/pin/verify.' } ] } ] }, { slug: 'oauth', title: 'OAuth 2.0', description: 'Регистрация приложения, Authorization Code Flow, scopes и примеры на разных языках.', sections: [ { id: 'register-app', title: 'Регистрация OAuth-приложения', blocks: [ { type: 'list', items: [ 'Войдите как супер-администратор в админ-панель', 'Перейдите в RBAC → OAuth приложения', 'Создайте клиент: name, redirectUris, scopes (openid, profile, email)', 'Confidential-клиент получит client_secret один раз — сохраните его' ] } ] }, { id: 'flow', title: 'Authorization Code Flow', blocks: [ { type: 'paragraph', text: '1) Пользователь авторизуется в IdP. 2) Ваше приложение перенаправляет на GET /oauth/authorize с clientId, redirectUri, scope, userId. 3) IdP возвращает redirectUrl с authorization code. 4) Backend обменивает code на токены через POST /oauth/token. 5) GET /oauth/userinfo возвращает профиль. Базовый URL (issuer) берётся из настройки PUBLIC_API_URL в админ-панели — примеры кода ниже подставляют его автоматически.' }, { type: 'callout', variant: 'info', title: 'OpenID Connect Discovery', text: 'Метаданные провайдера: GET {PUBLIC_API_URL}/.well-known/openid-configuration. В стороннем сервисе укажите issuer = PUBLIC_API_URL (например https://sso.example.ru/idp-api), а не URL authorization endpoint. Если API доступен через /idp-api на домене frontend, issuer тоже должен включать этот путь.' }, { type: 'callout', variant: 'warning', title: 'Безопасность', text: 'client_secret никогда не храните во frontend. Обмен code → token выполняйте только на сервере.' } ] }, { id: 'examples', title: 'Примеры интеграции', blocks: [{ type: 'oauth-examples' }] }, { id: 'scopes', title: 'Scopes', blocks: [ { type: 'table', headers: ['Scope', 'Доступ'], rows: [ ['openid', 'Идентификатор пользователя (sub)'], ['profile', 'displayName, avatar, bio'], ['email', 'Основная и резервная почта'], ['phone', 'Телефоны пользователя'] ] } ] } ] }, { slug: 'ldap', title: 'LDAP / LDAPS', description: 'Корпоративный вход через Active Directory.', sections: [ { id: 'setup', title: 'Настройка в админке', blocks: [ { type: 'table', headers: ['Ключ SystemSetting', 'Описание'], rows: [ ['LDAP_ENABLED', 'Включить кнопку «Войти через LDAP»'], ['LDAP_USE_LDAPS', 'LDAPS на порту 636'], ['LDAP_HOST', 'DC-1.domain.local,DC-2.domain.local'], ['LDAP_BIND_USERNAME', 'Логин сервисной УЗ (mvkadmin)'], ['LDAP_BASE_DN', 'OU=Users,DC=domain,DC=local'], ['LDAP_BIND_PASSWORD', 'Пароль сервисной УЗ'] ] } ] }, { id: 'login', title: 'Примеры LDAP-входа', blocks: [ { type: 'paragraph', text: 'Endpoint POST /auth/ldap/login доступен при включённой настройке LDAP_ENABLED.' }, { type: 'auth-ldap-examples' } ] } ] }, { slug: 'sessions', title: 'Сессии, PIN и удаление аккаунта', description: 'Управление устройствами, PIN-блокировка, отзыв сессий и отложенное удаление профиля.', sections: [ { id: 'totp', title: 'TOTP (Google Authenticator)', blocks: [ { type: 'paragraph', text: 'Двухфакторная аутентификация через TOTP заменяет SMS/email OTP при входе. Управление устройствами и отзыв сессий — через те же endpoints, что описаны ниже.' }, { type: 'list', items: [ 'GET /security/users/{userId}/totp/status', 'POST /security/users/{userId}/totp/setup | enable | disable', 'POST /auth/totp/begin и POST /auth/totp/verify — вход с аутентификатором' ] } ] }, { id: 'pin', title: 'PIN-код', blocks: [ { type: 'paragraph', text: 'PIN хранится как bcrypt hash. Таймаут блокировки читается из SystemSetting PIN_LOCK_TIMEOUT_MINUTES — значение не захардкожено во frontend.' }, { type: 'list', items: [ 'PIN_DELETE_GRACE_MINUTES — задержка перед окончательным удалением PIN после запроса', 'PIN_REQUIRE_ON_DELETE — требовать текущий PIN при запросе удаления защиты' ] } ] }, { id: 'account-deletion', title: 'Удаление аккаунта', blocks: [ { type: 'paragraph', text: 'Пользователь может запланировать удаление профиля в личном кабинете: раздел «Данные» (/data) → «Удалить профиль». Аккаунт не удаляется мгновенно — начинается период ожидания, настраиваемый администратором.' }, { type: 'table', headers: ['Ключ SystemSetting', 'Описание', 'По умолчанию'], rows: [ ['ACCOUNT_DELETE_GRACE_DAYS', 'Через сколько дней после запроса окончательно удалить аккаунт', '30'] ] }, { type: 'list', items: [ 'POST /profile/users/{userId}/self-delete — запланировать удаление (только свой профиль)', 'GET /profile/users/{userId}/self-delete/status — дата окончательного удаления и статус pending', 'POST /profile/users/{userId}/self-delete/cancel — отменить запрос до истечения срока', 'До истечения срока пользователь может входить и пользоваться сервисом как обычно', 'На странице /data отображается предупреждение с датой удаления и кнопкой отмены' ] }, { type: 'callout', variant: 'warning', title: 'Что происходит при финализации', text: 'Фоновый планировщик sso-core (каждые 5 минут) находит аккаунты с истёкшим сроком ожидания и выполняет soft-delete: анонимизация email/телефона/username, отзыв сессий, снятие ролей. Семьи, где пользователь — создатель, удаляются полностью (участники, чаты, медиа). Из остальных семей пользователь исключается.' } ] }, { id: 'devices', title: 'Устройства и сессии', blocks: [ { type: 'list', items: [ 'GET /security/users/{userId}/devices — список устройств', 'POST /security/users/{userId}/sessions/{sessionId}/revoke — выход с устройства', 'POST /security/users/{userId}/revoke-all-sessions — выход везде' ] } ] } ] }, { slug: 'family-chat', title: 'Семья и чат', description: 'Семейные группы, приглашения, чат, удаление семьи и realtime через WebSocket.', sections: [ { id: 'family', title: 'Семейные группы', blocks: [ { type: 'paragraph', text: 'Пользователь создаёт семью, приглашает участников по email/телефону/логину или через поиск в интерфейсе. Лимит участников задаётся в SystemSetting MAX_FAMILY_MEMBERS (по умолчанию 6). При создании семьи автоматически создаётся общий чат «Общий чат».' }, { type: 'list', items: [ 'POST /family/groups — создать семью', 'GET /family/users/{userId}/groups — список семей пользователя', 'PATCH /family/groups/{groupId} — переименовать (только участники, название — создатель)', 'POST /family/groups/{groupId}/invites — отправить приглашение', 'GET /family/groups/{groupId}/invite-search?q=... — поиск пользователей для приглашения' ] } ] }, { id: 'family-leave', title: 'Выход и исключение участников', blocks: [ { type: 'paragraph', text: 'Участник может выйти из семьи самостоятельно; создатель семьи может исключить любого участника, кроме себя. При выходе или исключении пользователь удаляется из всех чатов этой семьи.' }, { type: 'list', items: [ 'DELETE /family/members/{memberId} — выход (если memberId свой) или исключение (если запрос от создателя семьи)', 'Создателя семьи (role: owner) удалить через этот endpoint нельзя — только удалить всю семью целиком' ] } ] }, { id: 'family-delete', title: 'Удаление семьи', blocks: [ { type: 'paragraph', text: 'Только создатель семьи может полностью удалить группу. В интерфейсе: страница семьи → «Участники семьи» → «Удалить семью». Действие необратимо.' }, { type: 'list', items: [ 'DELETE /family/groups/{groupId} — удалить семью (только ownerId === текущий пользователь)', 'Все участники исключаются из семьи', 'Все приглашения (FamilyInvite) удаляются', 'Все чаты семьи (ChatRoom), сообщения, опросы и голоса удаляются каскадом', 'Медиа в MinIO (аватар семьи, аватары чатов, вложения сообщений) удаляются best-effort', 'Остальным участникам отправляется уведомление family_group_deleted и событие WebSocket' ] }, { type: 'callout', variant: 'warning', title: 'Без передачи владения', text: 'Перед удалением семьи нельзя «передать» роль создателя другому участнику — только полное удаление группы или выход участников по отдельности.' } ] }, { id: 'chat', title: 'Чат и WebSocket', blocks: [ { type: 'list', items: [ 'REST: /chat/groups/{groupId}/rooms, /chat/rooms/{roomId}/messages', 'WebSocket: ws://localhost:8085/ws с JWT в query или заголовке', 'Медиа чата: presigned upload + защищённый stream с Authorization', 'События realtime: chat_message, chat_message_updated, chat_message_deleted, family_group_deleted' ] } ] } ] }, { slug: 'api-reference', title: 'Справочник API', description: 'Полный список REST endpoints Lendry ID API. Swagger: /api на api-gateway.', sections: [ { id: 'endpoints', title: 'Endpoints', blocks: [{ type: 'api-reference' }] }, { id: 'swagger', title: 'OpenAPI / Swagger', blocks: [ { type: 'paragraph', text: 'Интерактивная документация доступна по адресу http://localhost:3000/api (Swagger UI). Все DTO и схемы генерируются автоматически из NestJS decorators.' } ] } ] } ]; export function getDocPage(slug: string): DocPage | undefined { return docPages.find((page) => page.slug === slug); } export function getAllDocSlugs() { return docPages.map((page) => page.slug); }