From 0f1f89776bca2d283ae007ae7d9f1770124a9c8a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 4 Sep 2026 14:11:34 +0700 Subject: [PATCH] feat(cdn): integrate CDN Manager into the application - Updated environment configurations to include CDN Manager in the RETURN_TO_ALLOWLIST. - Enhanced target app resolution to recognize CDN-related hosts. - Added CDN Manager to the application switcher and updated relevant documentation. - Included tests to verify the correct mapping of CDN hosts. Co-authored-by: Cursor --- .env.example | 5 +- README.md | 1 + apps/api/src/lib/target-app.ts | 7 + apps/api/test/target-app.test.ts | 6 + apps/web/src/components/layout/apps-menu.tsx | 1 + .../components/reui-kit/audit-log-helpers.ts | 1 + apps/web/src/routes/_auth.apps.tsx | 1 + deploy/env.traefik.example | 2 +- docs/deploy-traefik.md | 2 +- docs/integrate-audit-ingest.md | 4 +- docs/integrate-cdnmanager.md | 141 ++++++++++++++++++ docs/ui-design-contract.md | 2 +- packages/shared/src/contracts/app-switcher.ts | 2 + packages/shared/src/contracts/audit.ts | 3 +- packages/shared/src/contracts/auth.ts | 22 ++- 15 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 docs/integrate-cdnmanager.md diff --git a/.env.example b/.env.example index 5369c88..e00697a 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ ADMIN_PASSWORD=admin ADMIN_NAME=Admin # Allowed return_to hosts (comma-separated), e.g. .shnt.top or full origins -RETURN_TO_ALLOWLIST=.shnt.top,localhost,private,http://localhost:5173,http://localhost:5174 +RETURN_TO_ALLOWLIST=.shnt.top,localhost,private,http://localhost:5173,http://localhost:5174,http://localhost:5176 # ReUI PRO (apps/web/components.json → @reui Authorization) # Ключ: https://reui.io/docs/license-setup — класть в apps/web/.env.local (gitignored) @@ -24,7 +24,8 @@ REUI_LICENSE_KEY= # Local app URLs for SSO Open (apps/web/.env.local) # VITE_VPS_APP_URL=http://localhost:5173 -# VITE_RETURN_TO_ALLOWLIST=.shnt.top,localhost,http://localhost:5173 +# VITE_CDN_APP_URL=http://localhost:5176 +# VITE_RETURN_TO_ALLOWLIST=.shnt.top,localhost,http://localhost:5173,http://localhost:5176 # Server SERVER_PORT=8080 diff --git a/README.md b/README.md index a0a202a..dada9be 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ pnpm --filter web dev # :5175 См. [`docs/integrate-evobgp.md`](docs/integrate-evobgp.md) — EvoBGP (`bgp:*`). См. [`docs/integrate-evofirewall.md`](docs/integrate-evofirewall.md) — EvoFirewall (`fw:*`). +См. [`docs/integrate-cdnmanager.md`](docs/integrate-cdnmanager.md) — CDN Manager (`cdn:*`, порт Vite `5176`). См. [`docs/integrate-technitium.md`](docs/integrate-technitium.md) — Technitium DNS (`dns:*`, OIDC IdP). Корень: diff --git a/apps/api/src/lib/target-app.ts b/apps/api/src/lib/target-app.ts index eacd04d..56947fc 100644 --- a/apps/api/src/lib/target-app.ts +++ b/apps/api/src/lib/target-app.ts @@ -51,6 +51,13 @@ export function targetAppFromReturnTo( ) { return 'dns' } + if ( + /\bcdn\b/.test(hay) || + host.includes('cdnmanager') || + host.includes('cdn-manager') + ) { + return 'cdn' + } return 'portal' } diff --git a/apps/api/test/target-app.test.ts b/apps/api/test/target-app.test.ts index cfecc99..44daac7 100644 --- a/apps/api/test/target-app.test.ts +++ b/apps/api/test/target-app.test.ts @@ -21,4 +21,10 @@ describe('targetAppFromReturnTo', () => { targetAppFromReturnTo('https://auth.shnt.top/oauth/authorize'), ).toBe('portal') }) + + it('maps cdn host', () => { + expect(targetAppFromReturnTo('https://cdn.shnt.top/auth/callback')).toBe( + 'cdn', + ) + }) }) diff --git a/apps/web/src/components/layout/apps-menu.tsx b/apps/web/src/components/layout/apps-menu.tsx index a8813c9..1492389 100644 --- a/apps/web/src/components/layout/apps-menu.tsx +++ b/apps/web/src/components/layout/apps-menu.tsx @@ -28,6 +28,7 @@ const APP_ICONS: Record< bgp: NetworkIcon, fw: ShieldIcon, dns: GlobeIcon, + cdn: CloudIcon, } export function AppsMenu() { diff --git a/apps/web/src/components/reui-kit/audit-log-helpers.ts b/apps/web/src/components/reui-kit/audit-log-helpers.ts index 0f0bc86..e9dca44 100644 --- a/apps/web/src/components/reui-kit/audit-log-helpers.ts +++ b/apps/web/src/components/reui-kit/audit-log-helpers.ts @@ -43,6 +43,7 @@ export const SOURCE_APP_OPTIONS: { { value: 'bgp', label: 'EvoBGP' }, { value: 'fw', label: 'EvoFirewall' }, { value: 'dns', label: 'Technitium DNS' }, + { value: 'cdn', label: 'CDN Manager' }, ] export const severityVariant: Record = { diff --git a/apps/web/src/routes/_auth.apps.tsx b/apps/web/src/routes/_auth.apps.tsx index b4eec36..dd8a834 100644 --- a/apps/web/src/routes/_auth.apps.tsx +++ b/apps/web/src/routes/_auth.apps.tsx @@ -30,6 +30,7 @@ const APP_ICONS: Record = { bgp: GlobeIcon, fw: ShieldIcon, dns: GlobeIcon, + cdn: CloudIcon, } async function openApp( diff --git a/deploy/env.traefik.example b/deploy/env.traefik.example index 7b95ebf..0f7c56a 100644 --- a/deploy/env.traefik.example +++ b/deploy/env.traefik.example @@ -24,7 +24,7 @@ ADMIN_EMAIL=admin@shnt.top ADMIN_PASSWORD= ADMIN_NAME=Admin # Include Technitium origin if used (e.g. https://dns.shnt.top) -RETURN_TO_ALLOWLIST=.shnt.top,https://vps.shnt.top,https://cfdm.shnt.top,https://bgp.shnt.top,https://fw.shnt.top,https://dns.shnt.top +RETURN_TO_ALLOWLIST=.shnt.top,https://vps.shnt.top,https://cfdm.shnt.top,https://bgp.shnt.top,https://fw.shnt.top,https://dns.shnt.top,https://cdn.shnt.top LOG_LEVEL=info NODE_ENV=production # WebAuthn / passkeys (defaults from ISSUER hostname + origin) diff --git a/docs/deploy-traefik.md b/docs/deploy-traefik.md index bd9f7ec..1996b32 100644 --- a/docs/deploy-traefik.md +++ b/docs/deploy-traefik.md @@ -179,7 +179,7 @@ VITE_AUTH_ENABLED=true VITE_AUTH_PORTAL_URL=https://auth.shnt.top ``` -См. [integrate-vps-tracker.md](integrate-vps-tracker.md), [integrate-cfdm.md](integrate-cfdm.md), [integrate-evobgp.md](integrate-evobgp.md), [integrate-evofirewall.md](integrate-evofirewall.md). +См. [integrate-vps-tracker.md](integrate-vps-tracker.md), [integrate-cfdm.md](integrate-cfdm.md), [integrate-evobgp.md](integrate-evobgp.md), [integrate-evofirewall.md](integrate-evofirewall.md), [integrate-cdnmanager.md](integrate-cdnmanager.md). Logout SSO: `https://auth.shnt.top/logout`. ### Technitium DNS (OIDC) diff --git a/docs/integrate-audit-ingest.md b/docs/integrate-audit-ingest.md index e192ef2..dd16110 100644 --- a/docs/integrate-audit-ingest.md +++ b/docs/integrate-audit-ingest.md @@ -36,7 +36,7 @@ Content-Type: application/json ``` - `events`: 1–50 за запрос -- `source_app`: `vps` | `cfdm` | `bgp` | `fw` (не `portal`) +- `source_app`: `vps` | `cfdm` | `bgp` | `fw` | `dns` | `cdn` (не `portal`) - `event_id`: идемпотентность (дубликаты → `duplicates++`) - Ответ: `{ "accepted": N, "duplicates": M }` @@ -45,7 +45,7 @@ Content-Type: application/json | Где | Переменная | |-----|------------| | auth-portal | `AUDIT_INGEST_SECRET` | -| apps (vps / cfdm / bgp / fw) | `AUTH_PORTAL_URL` + `AUTH_AUDIT_INGEST_SECRET` (тот же секрет) | +| apps (vps / cfdm / bgp / fw / cdn) | `AUTH_PORTAL_URL` + `AUTH_AUDIT_INGEST_SECRET` (тот же секрет) | Dev default secret: `dev-audit-ingest-secret`. diff --git a/docs/integrate-cdnmanager.md b/docs/integrate-cdnmanager.md new file mode 100644 index 0000000..271140a --- /dev/null +++ b/docs/integrate-cdnmanager.md @@ -0,0 +1,141 @@ +# Интеграция auth-portal ↔ CDN Manager + +Единый вход: пользователь логинится на auth-portal, получает JWT, переходит в CDNManager с токеном в URL fragment. CDNManager API проверяет JWT и права `cdn:*`. + +## Архитектура + +``` +Browser → CDNManager UI (нет token) + → redirect AUTH_PORTAL_URL/?return_to=…/auth/callback + → login + → redirect return_to#access_token=… + → CDNManager /auth/callback сохраняет token (cdnmanager_token) + → API Authorization: Bearer … +``` + +Общий секрет: `JWT_SECRET` / `AUTH_JWT_SECRET` (HS256). Issuer: `ISSUER` / `AUTH_ISSUER`. + +App id в портале: **`cdn`** (каталог permissions). + +## Локальный запуск + +### 1. auth-portal + +```bash +cd auth-portal +pnpm install +# JWT_SECRET=dev-secret-change-me +# RETURN_TO_ALLOWLIST=.shnt.top,localhost,http://localhost:5173,…,http://localhost:5176 +pnpm --filter @authportal/api dev # :8080 +pnpm --filter web dev # :5175 +``` + +В `apps/web/.env.local` (опционально для App Switcher / SSO open): + +```env +VITE_CDN_APP_URL=http://localhost:5176 +``` + +Bootstrap: `admin@shnt.top` / `admin`. В админке выдайте app **cdn** и permissions `cdn:*`. + +### 2. CDNManager + +```bash +cd CDNManager +pnpm install +``` + +Корень / API: + +```env +AUTH_REQUIRED=true +AUTH_JWT_SECRET=dev-secret-change-me +AUTH_ISSUER=https://auth.shnt.top +AUTH_PORTAL_URL=http://localhost:5175 +CLOUDFLARE_API_TOKEN= +``` + +`apps/web/.env.local`: + +```env +VITE_AUTH_ENABLED=true +VITE_AUTH_PORTAL_URL=http://localhost:5175 +``` + +Порт Vite web — `5176` (`apps/web/vite.config.ts`). Добавьте origin в `RETURN_TO_ALLOWLIST` портала. + +```bash +pnpm --filter @cdnmanager/api dev +pnpm --filter web dev +``` + +Откройте CDNManager → редирект на portal → после логина NavUser показывает имя/email. + +## Permissions ↔ API / UI + +Иерархия: `admin` ⊃ `write` ⊃ `read` в рамках одной секции. + +| Permission | API | UI | +|------------|-----|-----| +| `cdn:dashboard:read` | GET `/api/v1/dashboard/*`, `/topology` | `/` | +| `cdn:nodes:read` | GET `/api/v1/nodes*`, `/locations` | `/nodes` | +| `cdn:nodes:write` | POST/PATCH/DELETE nodes | create/edit нод | +| `cdn:aliases:read` | GET `/api/v1/aliases*` | `/aliases` | +| `cdn:aliases:write` | POST/PATCH aliases, retarget | create / Retarget | +| `cdn:zones:read` | GET `/api/v1/zones*` | `/zones` | +| `cdn:zones:write` | POST/PATCH zones, BIND export | создать зону | +| `cdn:sync:write` | POST `…/sync`, `…/apply` | Sync / Apply | +| `cdn:topology:read` | GET `/api/v1/topology` | `/topology` | +| `cdn:settings:admin` | GET/PATCH `/api/v1/settings` | `/settings/*` | + +Без app `cdn` в JWT `apps` → **403** на защищённые `/api/v1/*`. + +`AUTH_REQUIRED=false` — локальный login (`ADMIN_*`) для тестов/dev без portal; UI `/login`. + +## App Switcher + +Публичный конфиг: `GET {AUTH_PORTAL_URL}/api/v1/app-switcher` (CORS open). CDNManager chrome (`AppSwitcher` / `AppsMenu`) читает его через `ensureAuthConfig().portalUrl`. + +Редактор только на портале: **Админка → Ссылки приложений** (`/admin/apps`). В CDNManager Settings → Интеграции — read-only ссылка на портал. + +`CURRENT_APP_ID = cdn`. Если в JWT есть `apps[]` — в меню только пересечение с каталогом. + +## Audit ingest + +Dual-write локального журнала в portal: [`integrate-audit-ingest.md`](./integrate-audit-ingest.md) (`source_app: cdn`). + +## UI аккаунта + +SidebarFooter → **NavUser**: Настройки, Тема, Выйти → `AUTH_PORTAL_URL/logout`. + +## Logout (SSO) + +Очистить `cdnmanager_token` → редирект на **`/logout`** портала (не на `/?return_to=…` — иначе portal сразу выдаст новый SSO-токен). + +## Production (Docker) + +Рекомендуется Traefik-стек в репозитории CDNManager: `docs/deploy-traefik.md` +(`deploy/docker-compose.traefik.yml` + `deploy/env.traefik.example`). + +Ключевые env контейнера: + +```env +AUTH_REQUIRED=true +AUTH_JWT_SECRET=<тот же JWT_SECRET портала> +AUTH_ISSUER=https://auth.shnt.top +AUTH_PORTAL_URL=https://auth.shnt.top +AUTH_AUDIT_INGEST_SECRET= +CLOUDFLARE_API_TOKEN= +``` + +В portal: `RETURN_TO_ALLOWLIST` включает `https://cdn.shnt.top` (или ваш origin). + +## Troubleshooting + +| Симптом | Причина | +|---------|---------| +| SSO loop / «Сессия не принята» | разный `JWT_SECRET` или `ISSUER` у portal и CDNManager | +| 403 «Нет доступа к приложению» | у пользователя нет app `cdn` в portal | +| 403 «Недостаточно прав» | нет нужного `cdn:…` permission | +| return_to rejected | origin CDNManager не в `RETURN_TO_ALLOWLIST` | +| «Выйти» сразу возвращает в CDNManager | клиент должен открывать `/logout`, не login с `return_to` | diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 111b432..3d197d8 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -43,7 +43,7 @@ Nav groups Auth Portal: - **Портал:** Приложения (`/apps`) - **Админ** (только `is_admin`): Пользователи (`/admin`), Журнал (`/admin/audit`), Ссылки приложений (`/admin/apps`) -App Switcher: `portal_settings.app_switcher_json` → public `GET /api/v1/app-switcher`, admin `GET/PUT /api/v1/admin/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw` · `dns`. +App Switcher: `portal_settings.app_switcher_json` → public `GET /api/v1/app-switcher`, admin `GET/PUT /api/v1/admin/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw` · `dns` · `cdn`. ## MCP workflow diff --git a/packages/shared/src/contracts/app-switcher.ts b/packages/shared/src/contracts/app-switcher.ts index ae7b1de..e1a8d09 100644 --- a/packages/shared/src/contracts/app-switcher.ts +++ b/packages/shared/src/contracts/app-switcher.ts @@ -47,6 +47,7 @@ const DEFAULT_ICONS: Record = { bgp: 'globe', fw: 'server', dns: 'globe', + cdn: 'cloud', } const DEFAULT_AUTH_MODE: Record = { @@ -55,6 +56,7 @@ const DEFAULT_AUTH_MODE: Record = { bgp: 'jwt', fw: 'jwt', dns: 'oidc', + cdn: 'jwt', } /** Seed / fallback when DB is empty. */ diff --git a/packages/shared/src/contracts/audit.ts b/packages/shared/src/contracts/audit.ts index f1084fb..eb8c30f 100644 --- a/packages/shared/src/contracts/audit.ts +++ b/packages/shared/src/contracts/audit.ts @@ -11,6 +11,7 @@ export const AUDIT_SOURCE_APPS = [ 'bgp', 'fw', 'dns', + 'cdn', ] as const export type AuditSourceApp = (typeof AUDIT_SOURCE_APPS)[number] export const auditSourceAppSchema = z.enum(AUDIT_SOURCE_APPS) @@ -106,7 +107,7 @@ export type AuditPurgeResponse = z.infer export const ingestAuditEventSchema = z.object({ event_id: z.string().min(1).max(128), - source_app: z.enum(['vps', 'cfdm', 'bgp', 'fw', 'dns']), + source_app: z.enum(['vps', 'cfdm', 'bgp', 'fw', 'dns', 'cdn']), action: z.string().min(1).max(200), severity: auditSeveritySchema.optional(), actor_user_id: z.string().nullable().optional(), diff --git a/packages/shared/src/contracts/auth.ts b/packages/shared/src/contracts/auth.ts index 32b556a..92b5e70 100644 --- a/packages/shared/src/contracts/auth.ts +++ b/packages/shared/src/contracts/auth.ts @@ -1,6 +1,6 @@ import { z } from 'zod' -export const APP_IDS = ['cfdm', 'vps', 'bgp', 'fw', 'dns'] as const +export const APP_IDS = ['cfdm', 'vps', 'bgp', 'fw', 'dns', 'cdn'] as const export type AppId = (typeof APP_IDS)[number] export const appIdSchema = z.enum(APP_IDS) @@ -57,6 +57,13 @@ export const APPS: AppMeta[] = [ url: 'https://dns.shnt.top', authMode: 'oidc', }, + { + id: 'cdn', + title: 'CDN Manager', + description: 'Флот DNS: ноды, алиасы, sync Cloudflare', + url: 'https://cdn.shnt.top', + authMode: 'jwt', + }, ] export type CatalogSection = { @@ -154,6 +161,19 @@ export const PERMISSION_CATALOG: AppPermissionCatalog[] = [ section('settings', 'Настройки', 'SSO и системные настройки', ['admin']), ], }, + { + appId: 'cdn', + title: 'CDN Manager', + sections: [ + section('dashboard', 'Панель', 'KPI и обзор', ['read']), + section('nodes', 'Ноды', 'Канонические хосты A/AAAA'), + section('aliases', 'Алиасы', 'CNAME и retarget'), + section('zones', 'Зоны', 'Cloudflare zones и BIND export'), + section('sync', 'Синхронизация', 'Pull/diff/apply DNS', ['write']), + section('topology', 'Топология', 'Схема флота', ['read']), + section('settings', 'Настройки', 'Naming, TTL, Cloudflare', ['admin']), + ], + }, ] export function permissionKey(