From bb17ad6f4d1833d882a11515874074db035ceb64 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 19 Jul 2026 01:00:37 +0700 Subject: [PATCH] feat(auth): enhance JWT claims and app switcher configuration - Added support for optional tenant IDs in JWT claims for user permissions. - Updated auth routes to include tenant information in the JWT payload. - Enhanced app switcher configuration to handle tenant IDs without exposing them publicly. - Improved documentation for EvoBGP tenant ID integration and its usage in JWT. --- apps/api/src/plugins/auth-guards.ts | 4 +++ apps/api/src/routes/auth.ts | 18 ++++++++++-- .../reui-kit/app-switcher-admin-editor.tsx | 16 ++++++++++ docs/integrate-evobgp.md | 13 +++++---- packages/shared/src/contracts/app-switcher.ts | 29 +++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) diff --git a/apps/api/src/plugins/auth-guards.ts b/apps/api/src/plugins/auth-guards.ts index 698faa4..d1f52d5 100644 --- a/apps/api/src/plugins/auth-guards.ts +++ b/apps/api/src/plugins/auth-guards.ts @@ -25,6 +25,8 @@ declare module '@fastify/jwt' { name: string apps: string[] permissions: string[] + tenants?: Record + bgp_tenant_id?: string is_admin?: boolean iss: string } @@ -34,6 +36,8 @@ declare module '@fastify/jwt' { name: string apps: string[] permissions: string[] + tenants?: Record + bgp_tenant_id?: string is_admin?: boolean iss: string } diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 34a239f..1af6c58 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -3,9 +3,12 @@ import { hash, verify } from '@node-rs/argon2' import { randomBytes } from 'node:crypto' import { PERMISSION_CATALOG, + allPermissionKeys, appsMetaFromSwitcher, loginRequestSchema, normalizePermissionKeys, + publicAppSwitcherConfig, + tenantsClaimForUser, type LoginResponse, } from '@authportal/shared' import { @@ -53,9 +56,16 @@ export async function authRoutes(app: FastifyInstance): Promise { } const apps = getUserApps(app.db, user.id) - const permissions = normalizePermissionKeys( + let permissions = normalizePermissionKeys( getUserPermissions(app.db, user.id), ) + // Portal admin gets full catalog in JWT so apps can rely on permissions + // even when UI also checks is_admin. + if (user.isAdmin) { + permissions = allPermissionKeys() + } + const switcher = getAppSwitcherConfig(app.db) + const tenants = tenantsClaimForUser(switcher, apps) const me = toMe(user, apps, permissions) const expiresAt = new Date( @@ -68,6 +78,8 @@ export async function authRoutes(app: FastifyInstance): Promise { name: user.name, apps, permissions, + tenants, + bgp_tenant_id: tenants.bgp, is_admin: user.isAdmin, iss: app.config.issuer, }, @@ -109,7 +121,9 @@ export async function authRoutes(app: FastifyInstance): Promise { }) /** Public — apps chrome (CFDM/VPS) fetch switcher URLs without portal JWT. */ - app.get('/api/v1/app-switcher', async () => getAppSwitcherConfig(app.db)) + app.get('/api/v1/app-switcher', async () => + publicAppSwitcherConfig(getAppSwitcherConfig(app.db)), + ) app.get( '/api/v1/auth/me', diff --git a/apps/web/src/components/reui-kit/app-switcher-admin-editor.tsx b/apps/web/src/components/reui-kit/app-switcher-admin-editor.tsx index 5c904d2..461b789 100644 --- a/apps/web/src/components/reui-kit/app-switcher-admin-editor.tsx +++ b/apps/web/src/components/reui-kit/app-switcher-admin-editor.tsx @@ -92,6 +92,22 @@ export function AppSwitcherAdminEditor({ placeholder="https://…" /> + {appId === 'bgp' ? ( + + + EvoBGP tenant ID + + +

+ Попадает в JWT (`bgp_tenant_id` / `tenants.bgp`). Публичный + switcher его не отдаёт. +

+
+ ) : null} Описание diff --git a/docs/integrate-evobgp.md b/docs/integrate-evobgp.md index e9d1060..f5eae47 100644 --- a/docs/integrate-evobgp.md +++ b/docs/integrate-evobgp.md @@ -69,7 +69,8 @@ AUTH_REQUIRED=true AUTH_JWT_SECRET=dev-secret-change-me AUTH_ISSUER=https://auth.shnt.top AUTH_PORTAL_URL=http://localhost:5175 -EVOBGP_PORTAL_TENANT_ID= +# Опционально, если tenant не задан в portal /admin/apps для bgp: +# EVOBGP_PORTAL_TENANT_ID= ``` ```env @@ -78,10 +79,12 @@ VITE_AUTH_ENABLED=true VITE_AUTH_PORTAL_URL=http://localhost:5175 ``` +В portal **Админ → Приложения → BGP** укажите **EvoBGP tenant ID** (UUID из БД / лога `DemoIDs` / API-ключа). Он попадёт в JWT как `bgp_tenant_id`. Portal `is_admin` получает полный каталог `bgp:*` в JWT. + ## App Switcher -Публичный конфиг: `GET {AUTH_PORTAL_URL}/api/v1/app-switcher`. -`CURRENT_APP_ID = bgp`. Редактор ссылок — portal `/admin/apps`. +Публичный конфиг: `GET {AUTH_PORTAL_URL}/api/v1/app-switcher` (без `tenantId`). +`CURRENT_APP_ID = bgp`. Редактор ссылок и tenant — portal `/admin/apps`. ## Logout (SSO) @@ -93,6 +96,6 @@ VITE_AUTH_PORTAL_URL=http://localhost:5175 |---------|---------| | 401 на API | Нет/битый Bearer; разные `JWT_SECRET` | | 403 нет доступа к приложению | В portal не выдан app `bgp` | -| 403 на раздел | Нет `bgp:
:…` | -| JWT без tenant | Не задан `EVOBGP_PORTAL_TENANT_ID` | +| 403 на раздел | Нет `bgp:
:…` (у admin после обновления портала — полный каталог; перелогиньтесь) | +| portal tenant not configured | Нет tenant в JWT и нет `EVOBGP_PORTAL_TENANT_ID` | | return_to rejected | origin EvoBGP не в `RETURN_TO_ALLOWLIST` | diff --git a/packages/shared/src/contracts/app-switcher.ts b/packages/shared/src/contracts/app-switcher.ts index d5f46fe..2bfcb8d 100644 --- a/packages/shared/src/contracts/app-switcher.ts +++ b/packages/shared/src/contracts/app-switcher.ts @@ -18,6 +18,8 @@ export const appSwitcherEntrySchema = z.object({ shortcut: z.string().optional(), enabled: z.boolean(), sort: z.number().int().optional(), + /** App-scoped tenant (e.g. EvoBGP UUID) — goes into JWT, not public switcher. */ + tenantId: z.string().optional(), }) export const appSwitcherConfigSchema = z.object({ @@ -73,6 +75,7 @@ export function normalizeAppSwitcherConfig( sort: existing?.sort ?? index, enabled: existing?.enabled ?? true, icon: existing?.icon ?? fallback.icon, + tenantId: existing?.tenantId?.trim() || undefined, } }).sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)) @@ -82,6 +85,32 @@ export function normalizeAppSwitcherConfig( } } +/** Public GET must not expose tenant IDs. */ +export function publicAppSwitcherConfig( + config: AppSwitcherConfig, +): AppSwitcherConfig { + const normalized = normalizeAppSwitcherConfig(config) + return { + menuLabel: normalized.menuLabel, + apps: normalized.apps.map(({ tenantId: _tid, ...rest }) => rest), + } +} + +/** Map appId → tenantId for JWT (only apps the user may access). */ +export function tenantsClaimForUser( + config: AppSwitcherConfig, + userApps: readonly string[], +): Record { + const allowed = new Set(userApps) + const out: Record = {} + for (const app of normalizeAppSwitcherConfig(config).apps) { + if (!allowed.has(app.id)) continue + const tid = app.tenantId?.trim() + if (tid) out[app.id] = tid + } + return out +} + /** AppMeta list with URLs from switcher store (for /apps + catalog). */ export function appsMetaFromSwitcher(config: AppSwitcherConfig): AppMeta[] { const normalized = normalizeAppSwitcherConfig(config)