feat(cdn): integrate CDN Manager into the application
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 57s
quality / api (push) Failing after 30s
CD / quality (push) Failing after 1m36s
CD / publish (push) Skipped
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 57s
quality / api (push) Failing after 30s
CD / quality (push) Failing after 1m36s
CD / publish (push) Skipped
- 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 <[email protected]>
This commit is contained in:
+3
-2
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
Корень:
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ const APP_ICONS: Record<
|
||||
bgp: NetworkIcon,
|
||||
fw: ShieldIcon,
|
||||
dns: GlobeIcon,
|
||||
cdn: CloudIcon,
|
||||
}
|
||||
|
||||
export function AppsMenu() {
|
||||
|
||||
@@ -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<AuditSeverity, BadgeProps['variant']> = {
|
||||
|
||||
@@ -30,6 +30,7 @@ const APP_ICONS: Record<AppId, typeof CloudIcon> = {
|
||||
bgp: GlobeIcon,
|
||||
fw: ShieldIcon,
|
||||
dns: GlobeIcon,
|
||||
cdn: CloudIcon,
|
||||
}
|
||||
|
||||
async function openApp(
|
||||
|
||||
@@ -24,7 +24,7 @@ [email protected]
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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: `[email protected]` / `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=<AUDIT_INGEST_SECRET портала>
|
||||
CLOUDFLARE_API_TOKEN=<Zone DNS Edit + Zone Read>
|
||||
```
|
||||
|
||||
В 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` |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ const DEFAULT_ICONS: Record<AppId, AppSwitcherIconName> = {
|
||||
bgp: 'globe',
|
||||
fw: 'server',
|
||||
dns: 'globe',
|
||||
cdn: 'cloud',
|
||||
}
|
||||
|
||||
const DEFAULT_AUTH_MODE: Record<AppId, AppAuthMode> = {
|
||||
@@ -55,6 +56,7 @@ const DEFAULT_AUTH_MODE: Record<AppId, AppAuthMode> = {
|
||||
bgp: 'jwt',
|
||||
fw: 'jwt',
|
||||
dns: 'oidc',
|
||||
cdn: 'jwt',
|
||||
}
|
||||
|
||||
/** Seed / fallback when DB is empty. */
|
||||
|
||||
@@ -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<typeof auditPurgeResponseSchema>
|
||||
|
||||
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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user