Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd7d9fd13c |
@@ -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).
|
||||
|
||||
Корень:
|
||||
|
||||
@@ -58,6 +58,13 @@ export function targetAppFromReturnTo(
|
||||
) {
|
||||
return 'cdn'
|
||||
}
|
||||
if (
|
||||
/\bmm\b/.test(hay) ||
|
||||
host.includes('mikrotik') ||
|
||||
host.includes('mmapp')
|
||||
) {
|
||||
return 'mm'
|
||||
}
|
||||
return 'portal'
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -27,4 +27,10 @@ describe('targetAppFromReturnTo', () => {
|
||||
'cdn',
|
||||
)
|
||||
})
|
||||
|
||||
it('maps mm host', () => {
|
||||
expect(targetAppFromReturnTo('https://mm.shnt.top/auth/callback')).toBe(
|
||||
'mm',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<AuditSeverity, BadgeProps['variant']> = {
|
||||
|
||||
@@ -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<AppId, typeof CloudIcon> = {
|
||||
fw: ShieldIcon,
|
||||
dns: GlobeIcon,
|
||||
cdn: CloudIcon,
|
||||
mm: LayoutDashboardIcon,
|
||||
}
|
||||
|
||||
async function openApp(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }`
|
||||
|
||||
|
||||
@@ -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` |
|
||||
@@ -48,6 +48,7 @@ const DEFAULT_ICONS: Record<AppId, AppSwitcherIconName> = {
|
||||
fw: 'server',
|
||||
dns: 'globe',
|
||||
cdn: 'cloud',
|
||||
mm: 'dashboard',
|
||||
}
|
||||
|
||||
const DEFAULT_AUTH_MODE: Record<AppId, AppAuthMode> = {
|
||||
@@ -57,6 +58,7 @@ const DEFAULT_AUTH_MODE: Record<AppId, AppAuthMode> = {
|
||||
fw: 'jwt',
|
||||
dns: 'oidc',
|
||||
cdn: 'jwt',
|
||||
mm: 'jwt',
|
||||
}
|
||||
|
||||
/** Seed / fallback when DB is empty. */
|
||||
|
||||
@@ -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<typeof auditPurgeResponseSchema>
|
||||
|
||||
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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user