diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts
index e072cc2..a612927 100644
--- a/apps/web/src/lib/auth.ts
+++ b/apps/web/src/lib/auth.ts
@@ -259,6 +259,30 @@ export function can(required: string): boolean {
return hasPermission(claims.permissions, required)
}
+/**
+ * Whether /v1/auth/session may manage API keys (`bgp:access:admin`).
+ * Mirrors backend `requirePerm` for JWT (is_admin / permissions) and API-key operator.
+ */
+export function sessionCanManageApiKeys(session: {
+ role?: string
+ kind?: string
+ is_admin?: boolean
+ permissions?: readonly string[]
+} | null | undefined): boolean {
+ if (!session) return false
+ const jwtPath =
+ session.kind === 'jwt' ||
+ session.is_admin === true ||
+ (session.permissions?.length ?? 0) > 0
+ if (jwtPath) {
+ return (
+ session.is_admin === true ||
+ hasPermission(session.permissions ?? [], 'bgp:access:admin')
+ )
+ }
+ return session.role === 'operator'
+}
+
/** Nav path → minimum permission to show the item. Sync with app-shell NAV. */
export function permissionForPath(pathname: string): string | null {
if (pathname === '/' || pathname.startsWith('/dashboard')) {
diff --git a/apps/web/src/routes/_auth/access.tsx b/apps/web/src/routes/_auth/access.tsx
index 74ea51b..674d051 100644
--- a/apps/web/src/routes/_auth/access.tsx
+++ b/apps/web/src/routes/_auth/access.tsx
@@ -11,6 +11,7 @@ import { PageHeader } from '@/components/page-header'
import { Badge } from '@/components/reui/badge'
import { SectionCards, type SectionCardItem } from '@/components/section-cards'
import { SectionCardsSkeleton } from '@/components/skeletons'
+import { sessionCanManageApiKeys } from '@/lib/auth'
import { authSessionQueryOptions } from '@/queries/auth'
import { apiKeysQueryOptions } from '@/queries/api-keys'
@@ -21,11 +22,11 @@ export const Route = createFileRoute('/_auth/access')({
function AccessComponent() {
const sessionQuery = useQuery(authSessionQueryOptions())
const session = sessionQuery.data ?? null
- const isOperator = session?.role === 'operator'
+ const canManageKeys = sessionCanManageApiKeys(session)
const keysQuery = useQuery({
...apiKeysQueryOptions(),
- enabled: isOperator,
+ enabled: canManageKeys,
})
const keys = keysQuery.data ?? []
@@ -73,16 +74,31 @@ function AccessComponent() {
function refetchAll() {
void sessionQuery.refetch()
- if (isOperator) void keysQuery.refetch()
+ if (canManageKeys) void keysQuery.refetch()
}
+ const sessionKindLabel =
+ session?.kind === 'jwt' ? 'Portal JWT' : session?.kind === 'apikey' ? 'API-ключ' : null
+
+ const sessionAccessLabel = (() => {
+ if (!session) return null
+ if (session.kind === 'jwt' || session.is_admin || (session.permissions?.length ?? 0) > 0) {
+ if (session.is_admin) return 'admin (portal)'
+ if (sessionCanManageApiKeys(session)) return 'bgp:access:admin'
+ return session.permissions?.length
+ ? session.permissions.slice(0, 3).join(', ')
+ : 'без access:admin'
+ }
+ return session.role || '—'
+ })()
+
return (
Обновить
@@ -94,36 +110,48 @@ function AccessComponent() {
{session ? (
+
+
Tenant
+
{session.tenant_id}
+
+
+
Доступ
+
{sessionAccessLabel}
+
+ {sessionKindLabel ? (
-
Tenant
-
{session.tenant_id}
+
Тип
+
{sessionKindLabel}
+ ) : null}
+ {session.email ? (
-
Роль
-
{session.role}
+
Email
+
{session.email}
+ ) : null}
) : (
- Не удалось определить сессию. Укажите токен в{' '}
-
- настройках
- {' '}
- (для dev-окружения — dev при включённом demo-seed).
- {sessionQuery.isError && sessionQuery.error instanceof Error ? (
- {sessionQuery.error.message}
- ) : null}
+ Не удалось определить сессию. Укажите токен в{' '}
+
+ настройках
+ {' '}
+ (для dev-окружения — dev при включённом demo-seed).
+ {sessionQuery.isError && sessionQuery.error instanceof Error ? (
+ {sessionQuery.error.message}
+ ) : null}
)}
- {isOperator ? (
+ {canManageKeys ? (
<>
{keysQuery.isLoading ? (
@@ -140,10 +168,9 @@ function AccessComponent() {
>
) : session ? (
- Управление API-ключами доступно только роли operator. Текущая роль:{' '}
- {session.role}. Для выдачи ключей войдите с
- operator-ключом или создайте ключ через API / переменную{' '}
- EVOBGP_API_KEYS.
+ Управление API-ключами доступно роли operator (API-ключ) или portal JWT
+ с is_admin / правом bgp:access:admin.
+ Текущий доступ: {sessionAccessLabel}.
) : null}
diff --git a/apps/web/src/types/api.ts b/apps/web/src/types/api.ts
index b1aa61a..092b96a 100644
--- a/apps/web/src/types/api.ts
+++ b/apps/web/src/types/api.ts
@@ -341,13 +341,20 @@ export type JobsResponse = Page
export type AppSettings = Record
// ---- Auth / API keys ----
+export type ApiKeyRole = 'viewer' | 'editor' | 'operator' | 'node'
+
+/** GET /v1/auth/session — API key has role; portal JWT uses kind/permissions/is_admin. */
export type AuthSession = {
tenant_id: string
- role: 'viewer' | 'editor' | 'operator' | 'node'
+ /** API-key role; empty for portal JWT sessions. */
+ role: ApiKeyRole | ''
+ kind?: 'apikey' | 'jwt'
+ user_id?: string
+ email?: string
+ permissions?: string[]
+ is_admin?: boolean
}
-export type ApiKeyRole = AuthSession['role']
-
export type ApiKey = {
id: string
name: string
diff --git a/docs/access.md b/docs/access.md
index 99b010a..2877e79 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -60,14 +60,18 @@ opkey|01ARZ3NDEKTSV4RRFFQ69G5FAV|operator,nodekey|01ARZ3NDEKTSV4RRFFQ69G5FAV|nod
### Управление через API и UI
-При подключённой БД operator может:
+При подключённой БД управлять ключами может:
-- `GET|POST /v1/api-keys`, `GET|PATCH|DELETE /v1/api-keys/{id}`, `POST /v1/api-keys/{id}/rotate` — см. OpenAPI, тег **API keys**.
-- В веб-панели: **Права доступа** (`/access`) → блок «API-ключи» (только для роли `operator`). Токен для браузера — в **Настройки** (`/settings`).
+- API-ключ с ролью **`operator`**, или
+- portal JWT с **`is_admin`** / правом **`bgp:access:admin`** (админ auth-portal).
+
+Эндпоинты: `GET|POST /v1/api-keys`, `GET|PATCH|DELETE /v1/api-keys/{id}`, `POST /v1/api-keys/{id}/rotate` — см. OpenAPI, тег **API keys**.
+
+В веб-панели: **Права доступа** (`/access`) → блок «API-ключи». Токен для браузера (API-key gate) — в **Настройки** (`/settings`).
Полный токен возвращается **один раз** в ответе `201` (создание) и `200` (ротация). В списках — только `prefix` (первые 8 символов). В БД хранится SHA-256 токена, не plaintext.
-`GET /v1/auth/session` — текущие `tenant_id` и `role` (для UI).
+`GET /v1/auth/session` — `tenant_id`, `kind` (`apikey`|`jwt`), для API-ключа — `role`; для JWT — `user_id`, `email`, `permissions`, `is_admin`.
### Роли
@@ -178,13 +182,13 @@ http://localhost:5173,http://127.0.0.1:5173,https://ui.example.com
## Краткая матрица (ориентир)
-| Действие | viewer | editor | operator | node |
-|----------|--------|--------|----------|------|
-| GET модули, ревизии, peers, speakers | да | да | да | нет |
-| POST/PATCH/DELETE CRUD сущностей | нет | да | да | нет |
-| apply, rollback, PATCH settings | нет | нет | да | нет |
-| Управление API-ключами (`/v1/api-keys`) | нет | нет | да | нет |
-| bundle, latest revision, enroll | нет | нет | нет | да |
+| Действие | viewer | editor | operator | node | portal admin / `bgp:access:admin` |
+|----------|--------|--------|----------|------|-----------------------------------|
+| GET модули, ревизии, peers, speakers | да | да | да | нет | по permissions |
+| POST/PATCH/DELETE CRUD сущностей | нет | да | да | нет | по permissions |
+| apply, rollback, PATCH settings | нет | нет | да | нет | `bgp:operations:admin` |
+| Управление API-ключами (`/v1/api-keys`) | нет | нет | да | нет | да |
+| bundle, latest revision, enroll | нет | нет | нет | да | нет |
Точные проверки по каждому маршруту — в коде `internal/httpapi` и в схеме безопасности операций в OpenAPI.
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 3490676..b849fbf 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -676,13 +676,32 @@ components:
AuthSession:
type: object
- required: [tenant_id, role]
+ required: [tenant_id, kind]
properties:
tenant_id:
$ref: "#/components/schemas/ResourceId"
+ kind:
+ type: string
+ enum: [apikey, jwt]
+ description: apikey — Bearer API key; jwt — portal SSO token.
role:
type: string
- enum: [viewer, editor, operator, node]
+ description: >
+ API-key role (viewer|editor|operator|node). Empty string for portal JWT sessions.
+ user_id:
+ type: string
+ description: JWT sub (portal sessions only).
+ email:
+ type: string
+ description: JWT email claim (portal sessions only).
+ permissions:
+ type: array
+ items:
+ type: string
+ description: JWT permissions (bgp:*); portal sessions only.
+ is_admin:
+ type: boolean
+ description: Portal is_admin claim; grants all bgp permissions.
ApiKey:
type: object