Публичный GET и admin PUT/UI /admin/apps; каталог и chrome читают URL из store. Co-authored-by: Cursor <[email protected]>
135 lines
5.8 KiB
Markdown
135 lines
5.8 KiB
Markdown
# Интеграция 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: `[email protected]` / `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.
|
||
|
||
## 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)
|