# Интеграция auth-portal ↔ VPS Tracker Единый вход: пользователь логинится на auth-portal, получает JWT, переходит в VPS Tracker с токеном в URL fragment. VPS API проверяет JWT и права `vps:*`. ## Архитектура ``` Browser → VPS UI (нет token) → redirect AUTH_PORTAL_URL/?return_to=http://localhost:5173/auth/callback → login → redirect return_to#access_token=… → VPS /auth/callback сохраняет token → API Authorization: Bearer … ``` Общий секрет: `JWT_SECRET` / `AUTH_JWT_SECRET` (HS256). Issuer: `ISSUER` / `AUTH_ISSUER`. ## Локальный запуск ### 1. auth-portal ```bash cd auth-portal pnpm install cp .env.example .env # JWT_SECRET=dev-secret-change-me # RETURN_TO_ALLOWLIST=.shnt.top,localhost,http://localhost:5173 pnpm --filter @authportal/api dev # :8080 pnpm --filter web dev # :5175 ``` В `apps/web/.env.local`: ```env VITE_VPS_APP_URL=http://localhost:5173 ``` Bootstrap: `admin@shnt.top` / `admin`. В админке выдайте app **vps** и нужные permissions. ### 2. vps-tracker ```bash cd vps-tracker pnpm install ``` Корень / API env (или shell): ```env AUTH_REQUIRED=true AUTH_JWT_SECRET=dev-secret-change-me AUTH_ISSUER=https://auth.shnt.top AUTH_PORTAL_URL=http://localhost:5175 ``` `apps/web/.env.local`: ```env VITE_AUTH_ENABLED=true VITE_AUTH_PORTAL_URL=http://localhost:5175 ``` ```bash pnpm --filter @cfdm/api dev pnpm --filter web dev # :5173 ``` Откройте http://localhost:5173 → редирект на portal → после логина обратно в VPS. ## Permissions ↔ API / UI Иерархия: `admin` ⊃ `write` ⊃ `read` в рамках одной секции. | Permission | API | UI | |------------|-----|-----| | `vps:dashboard:read` | `GET /api/dashboard/*` | `/dashboard` | | `vps:vps:read` | `GET /api/vps`, projects, reports-related reads | `/vps`, `/tariffs`, `/projects`, аналитика | | `vps:vps:write` | POST/PUT/PATCH/DELETE `/api/vps`, projects | create/edit/delete VPS | | `vps:accounts:read` | GET providers, provider-accounts | `/providers`, `/accounts` | | `vps:accounts:write` | мутации providers/accounts | формы аккаунтов | | `vps:payments:read` | GET payments, balance-ledger | `/payments`, `/balance` | | `vps:payments:write` | мутации payments / ledger | формы платежей | | `vps:sync:write` | POST `/api/sync/*`, GET sync status | `/sync-journal`, кнопка sync | | `vps:spaces:admin` | управление всеми пространствами, grants из main | space switcher (все spaces), share/assign | | `vps:settings:admin` | `/api/settings`, backup, audit, migrate | `/settings`, `/audit`, `/spaces` | ## Пространства (spaces) Данные VPS Tracker изолированы по `spaceId`. Заголовок **`X-Space-Id`**. - **space-main** — основное (админский пул); существующие данные мигрируют сюда. - **personal** — автосоздание `space-user-{userId}` при первом входе. - **Share (ACL)** — `POST /api/spaces/:id/vps/:vpsId/share` — VPS остаётся в исходном space. - **Assign** — `POST /api/spaces/:id/vps/:vpsId/assign` — перенос; `providerAccountId` сбрасывается. - Участники: `GET/POST/PATCH/DELETE /api/spaces/:id/members` (userId из auth-portal). - Env: `VPS_MAIN_SPACE_OWNER_USER_ID` — owner для main при bootstrap. Без app `vps` в JWT `apps` → **403** на весь `/api/*` (кроме health и CFDM integration). `AUTH_REQUIRED=false` — auth выключен (удобно для локальной разработки без portal); данные в `space-main`. ## App Switcher Публичный конфиг: `GET {AUTH_PORTAL_URL}/api/v1/app-switcher`. VPS chrome читает его через `ensureAuthConfig().portalUrl`; offline fallback — defaults с ids `cfdm` | `vps` | `bgp`. Редактор только на портале: **Админка → Ссылки приложений** (`/admin/apps`). В VPS Settings → Integrations — read-only ссылка. `CURRENT_APP_ID = vps`. Фильтр меню по JWT `apps[]` при наличии claims. ## Audit ingest Dual-write локального журнала в portal: [`integrate-audit-ingest.md`](./integrate-audit-ingest.md) (`source_app: vps`). ## Troubleshooting | Симптом | Причина | |---------|---------| | 401 на API | Нет/битый Bearer; разные `JWT_SECRET` | | 403 «нет доступа к приложению» | В portal не выдан app `vps` | | 403 на write | Только `*:read` в permissions | | Loop на login | `return_to` не в `RETURN_TO_ALLOWLIST` | | Infinite SSO / 429 | Просроченный JWT в portal localStorage; или разный `JWT_SECRET`/`ISSUER`. Portal чистит expired token; VPS блокирует повторный handoff 12с | | «Выйти» сразу возвращает в приложение | Старый клиент редиректил на `/?return_to=…` при живой portal-сессии. Нужен редирект на **`/logout`** (см. ниже) | | CORS | Portal и VPS на разных origin — fragment handoff не требует CORS для token; public app-switcher GET тоже CORS-open | ## Logout (SSO) «Выйти» в приложении: очистить локальный JWT → `AUTH_PORTAL_URL/logout` (без `return_to`). Портал на `/logout`: `POST /api/v1/auth/logout` (revoke refresh cookie) → `clearToken()` → форма логина. ## Production - Один `JWT_SECRET` в secret store обоих сервисов - `ISSUER=https://auth.shnt.top` - `RETURN_TO_ALLOWLIST=.shnt.top,https://vps.shnt.top` - `AUTH_REQUIRED=true`, `VITE_AUTH_ENABLED=true` - `VITE_VPS_APP_URL=https://vps.shnt.top` (portal)