From fd7d9fd13c81e85b41a32fe02debd200c5676c7a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 4 Sep 2026 20:38:27 +0700 Subject: [PATCH] =?UTF-8?q?feat(apps):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20MikrotikManager=20app=20id=20mm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Регистрация mm в каталоге приложений, permissions mm:*, target-app и docs. Co-authored-by: Cursor --- README.md | 1 + apps/api/src/lib/target-app.ts | 7 ++ apps/api/test/app-switcher.test.ts | 4 + apps/api/test/target-app.test.ts | 6 + apps/web/src/components/layout/apps-menu.tsx | 2 + .../components/reui-kit/audit-log-helpers.ts | 1 + apps/web/src/routes/_auth.apps.tsx | 2 + docs/deploy-traefik.md | 2 +- docs/integrate-audit-ingest.md | 2 +- docs/integrate-mikrotikmanager.md | 112 ++++++++++++++++++ packages/shared/src/contracts/app-switcher.ts | 2 + packages/shared/src/contracts/audit.ts | 3 +- packages/shared/src/contracts/auth.ts | 26 +++- 13 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 docs/integrate-mikrotikmanager.md diff --git a/README.md b/README.md index dada9be..e79feeb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,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-mikrotikmanager.md`](docs/integrate-mikrotikmanager.md) — MikrotikManager (`mm:*`). См. [`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 56947fc..27d70b7 100644 --- a/apps/api/src/lib/target-app.ts +++ b/apps/api/src/lib/target-app.ts @@ -58,6 +58,13 @@ export function targetAppFromReturnTo( ) { return 'cdn' } + if ( + /\bmm\b/.test(hay) || + host.includes('mikrotik') || + host.includes('mmapp') + ) { + return 'mm' + } return 'portal' } diff --git a/apps/api/test/app-switcher.test.ts b/apps/api/test/app-switcher.test.ts index 272bea0..09fa348 100644 --- a/apps/api/test/app-switcher.test.ts +++ b/apps/api/test/app-switcher.test.ts @@ -22,11 +22,15 @@ describe('app-switcher API', () => { 'cfdm', 'dns', 'fw', + 'mm', 'vps', ]) expect(body.apps.find((a) => a.id === 'cdn')).toMatchObject({ authMode: 'jwt', }) + expect(body.apps.find((a) => a.id === 'mm')).toMatchObject({ + authMode: 'jwt', + }) expect(body.apps.find((a) => a.id === 'dns')).toMatchObject({ authMode: 'oidc', }) diff --git a/apps/api/test/target-app.test.ts b/apps/api/test/target-app.test.ts index 44daac7..1bc6a9b 100644 --- a/apps/api/test/target-app.test.ts +++ b/apps/api/test/target-app.test.ts @@ -27,4 +27,10 @@ describe('targetAppFromReturnTo', () => { 'cdn', ) }) + + it('maps mm host', () => { + expect(targetAppFromReturnTo('https://mm.shnt.top/auth/callback')).toBe( + 'mm', + ) + }) }) diff --git a/apps/web/src/components/layout/apps-menu.tsx b/apps/web/src/components/layout/apps-menu.tsx index 1492389..97c1c3e 100644 --- a/apps/web/src/components/layout/apps-menu.tsx +++ b/apps/web/src/components/layout/apps-menu.tsx @@ -7,6 +7,7 @@ import { NetworkIcon, ShieldIcon, GlobeIcon, + LayoutDashboardIcon, } from 'lucide-react' import { APPS, type AppId } from '@authportal/shared' import { Button } from '@authportal/ui/components/button' @@ -29,6 +30,7 @@ const APP_ICONS: Record< fw: ShieldIcon, dns: GlobeIcon, cdn: CloudIcon, + mm: LayoutDashboardIcon, } 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 e9dca44..c0f4f05 100644 --- a/apps/web/src/components/reui-kit/audit-log-helpers.ts +++ b/apps/web/src/components/reui-kit/audit-log-helpers.ts @@ -44,6 +44,7 @@ export const SOURCE_APP_OPTIONS: { { value: 'fw', label: 'EvoFirewall' }, { value: 'dns', label: 'Technitium DNS' }, { value: 'cdn', label: 'CDN Manager' }, + { value: 'mm', label: 'MikrotikManager' }, ] export const severityVariant: Record = { diff --git a/apps/web/src/routes/_auth.apps.tsx b/apps/web/src/routes/_auth.apps.tsx index dd8a834..6bec808 100644 --- a/apps/web/src/routes/_auth.apps.tsx +++ b/apps/web/src/routes/_auth.apps.tsx @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { CloudIcon, GlobeIcon, + LayoutDashboardIcon, LayoutGridIcon, ServerIcon, ShieldIcon, @@ -31,6 +32,7 @@ const APP_ICONS: Record = { fw: ShieldIcon, dns: GlobeIcon, cdn: CloudIcon, + mm: LayoutDashboardIcon, } async function openApp( diff --git a/docs/deploy-traefik.md b/docs/deploy-traefik.md index 1996b32..064f7c0 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-cdnmanager.md](integrate-cdnmanager.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), [integrate-mikrotikmanager.md](integrate-mikrotikmanager.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 dd16110..255e22a 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` | `dns` | `cdn` (не `portal`) +- `source_app`: `vps` | `cfdm` | `bgp` | `fw` | `dns` | `cdn` | `mm` (не `portal`) - `event_id`: идемпотентность (дубликаты → `duplicates++`) - Ответ: `{ "accepted": N, "duplicates": M }` diff --git a/docs/integrate-mikrotikmanager.md b/docs/integrate-mikrotikmanager.md new file mode 100644 index 0000000..ad48540 --- /dev/null +++ b/docs/integrate-mikrotikmanager.md @@ -0,0 +1,112 @@ +# Интеграция auth-portal ↔ MikrotikManager + +Единый вход: пользователь логинится на auth-portal, получает JWT, переходит в MikrotikManager с токеном в URL fragment. Backend MM проверяет JWT и права `mm:*`. + +## Архитектура + +``` +Browser → MikrotikManager UI (нет token) + → redirect AUTH_PORTAL_URL/?return_to=…/auth/callback + → login + → redirect return_to#access_token=… + → MM /auth/callback сохраняет token (mmapp_token) + → API Authorization: Bearer … +``` + +Общий секрет: `JWT_SECRET` / `AUTH_JWT_SECRET` (HS256). Issuer: `ISSUER` / `AUTH_ISSUER`. + +App id в портале: **`mm`** (каталог permissions). + +## Локальный запуск + +### 1. auth-portal + +```bash +cd auth-portal +pnpm install +# JWT_SECRET=dev-secret-change-me +# RETURN_TO_ALLOWLIST=.shnt.top,localhost,http://localhost:3000 +pnpm --filter @authportal/api dev # :8080 +pnpm --filter web dev # :5175 +``` + +Bootstrap: `admin@shnt.top` / `admin`. В админке выдайте app **mm** и permissions `mm:*`. + +### 2. MikrotikManager + +```bash +cd MikrotikManager +npm install +``` + +Backend `backend/.env`: + +```env +AUTH_REQUIRED=true +AUTH_JWT_SECRET=dev-secret-change-me +AUTH_ISSUER=https://auth.shnt.top +AUTH_PORTAL_URL=http://localhost:5175 +CORS_ORIGIN=http://localhost:3000 +``` + +Frontend: `NEXT_PUBLIC_AUTH_PORTAL_URL=http://localhost:5175` (или runtime `GET /api/auth/config` через proxy). + +```bash +npm --prefix backend run dev +npm run dev +``` + +Откройте `http://localhost:3000` → редирект на portal → после логина NavUser показывает имя/email. + +## Permissions ↔ API / UI + +Иерархия: `admin` ⊃ `write` ⊃ `read` в рамках одной секции. + +| Permission | API (prefix) | UI | +|------------|--------------|-----| +| `mm:dashboard:read` | sidebar-counts, events (GET) | `/dashboard` | +| `mm:servers:read` / `write` | `/api/servers*` | `/servers` | +| `mm:filters:read` / `write` | `/api/filters*` | `/filters`, GRE | +| `mm:bgp:read` / `write` | `/api/bgp*` | BGP | +| `mm:uptime:read` / `write` | `/api/uptime*` | `/uptime` | +| `mm:traffic:read` / `write` | `/api/traffic*` | `/traffic` | +| `mm:alerts:read` / `write` | `/api/alerts*` | `/alerts` | +| `mm:backups:read` / `write` | `/api/backups*` | `/backups` | +| `mm:certificates:read` / `write` | `/api/certificates*` | certificates | +| `mm:network:read` / `write` | `/api/network*`, OSPF, recursive, probes | network | +| `mm:settings:admin` | `/api/system/*`, scheduler, evobgp settings | `/settings` | + +Без app `mm` в JWT `apps` → **403** на защищённые `/api/*`. + +`AUTH_REQUIRED=false` — API открыт (dev без portal). + +## App Switcher + +`CURRENT_APP_ID = mm`. Публичный конфиг: `GET {AUTH_PORTAL_URL}/api/v1/app-switcher`. + +## Logout (SSO) + +Очистить `mmapp_token` → редирект на **`{AUTH_PORTAL_URL}/logout`**. + +## Production + +См. общий стек CDN+MM: `deploy/docker-compose.cdn-mm.yml` / `deploy/env.cdn-mm.example`. + +```env +AUTH_REQUIRED=true +AUTH_JWT_SECRET=<тот же JWT_SECRET портала> +AUTH_ISSUER=https://auth.shnt.top +AUTH_PORTAL_URL=https://auth.shnt.top +CORS_ORIGIN=https://mm.shnt.top +``` + +В portal: `RETURN_TO_ALLOWLIST` включает `https://mm.shnt.top` (или `.shnt.top`). + +## Troubleshooting + +| Симптом | Причина | +|---------|---------| +| SSO loop | разный `JWT_SECRET` или `ISSUER` | +| 403 «Нет доступа к приложению» | нет app `mm` у пользователя | +| 403 «Недостаточно прав» | нет нужного `mm:…` | +| return_to rejected | origin MM не в `RETURN_TO_ALLOWLIST` | diff --git a/packages/shared/src/contracts/app-switcher.ts b/packages/shared/src/contracts/app-switcher.ts index e1a8d09..b99144c 100644 --- a/packages/shared/src/contracts/app-switcher.ts +++ b/packages/shared/src/contracts/app-switcher.ts @@ -48,6 +48,7 @@ const DEFAULT_ICONS: Record = { fw: 'server', dns: 'globe', cdn: 'cloud', + mm: 'dashboard', } const DEFAULT_AUTH_MODE: Record = { @@ -57,6 +58,7 @@ const DEFAULT_AUTH_MODE: Record = { fw: 'jwt', dns: 'oidc', cdn: 'jwt', + mm: 'jwt', } /** Seed / fallback when DB is empty. */ diff --git a/packages/shared/src/contracts/audit.ts b/packages/shared/src/contracts/audit.ts index eb8c30f..6727d0f 100644 --- a/packages/shared/src/contracts/audit.ts +++ b/packages/shared/src/contracts/audit.ts @@ -12,6 +12,7 @@ export const AUDIT_SOURCE_APPS = [ 'fw', 'dns', 'cdn', + 'mm', ] as const export type AuditSourceApp = (typeof AUDIT_SOURCE_APPS)[number] export const auditSourceAppSchema = z.enum(AUDIT_SOURCE_APPS) @@ -107,7 +108,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', 'cdn']), + source_app: z.enum(['vps', 'cfdm', 'bgp', 'fw', 'dns', 'cdn', 'mm']), 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 92b5e70..508e489 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', 'cdn'] as const +export const APP_IDS = ['cfdm', 'vps', 'bgp', 'fw', 'dns', 'cdn', 'mm'] as const export type AppId = (typeof APP_IDS)[number] export const appIdSchema = z.enum(APP_IDS) @@ -64,6 +64,13 @@ export const APPS: AppMeta[] = [ url: 'https://cdn.shnt.top', authMode: 'jwt', }, + { + id: 'mm', + title: 'MikrotikManager', + description: 'MikroTik: серверы, фильтры, BGP, uptime', + url: 'https://mm.shnt.top', + authMode: 'jwt', + }, ] export type CatalogSection = { @@ -174,6 +181,23 @@ export const PERMISSION_CATALOG: AppPermissionCatalog[] = [ section('settings', 'Настройки', 'Naming, TTL, Cloudflare', ['admin']), ], }, + { + appId: 'mm', + title: 'MikrotikManager', + sections: [ + section('dashboard', 'Дашборд', 'Обзор и KPI', ['read']), + section('servers', 'Серверы', 'MikroTik / CHR хосты'), + section('filters', 'Фильтры', 'Firewall и address-list'), + section('bgp', 'BGP', 'BGP-сессии и префиксы'), + section('uptime', 'Uptime', 'Пинги и speed-probes'), + section('traffic', 'Трафик', 'Интерфейсы и графики'), + section('alerts', 'Алерты', 'Правила и Telegram'), + section('backups', 'Бэкапы', 'Конфиги и расписание'), + section('certificates', 'Сертификаты', 'ACME / TLS'), + section('network', 'Сеть', 'GRE, OSPF, пути'), + section('settings', 'Настройки', 'Настройки приложения', ['admin']), + ], + }, ] export function permissionKey(