diff --git a/apps/api/src/plugins/auth-guards.ts b/apps/api/src/plugins/auth-guards.ts index 2e401a7..698faa4 100644 --- a/apps/api/src/plugins/auth-guards.ts +++ b/apps/api/src/plugins/auth-guards.ts @@ -6,7 +6,7 @@ import { type UserRow, } from '@authportal/db' import type { AppId, MeResponse } from '@authportal/shared' -import { APP_IDS } from '@authportal/shared' +import { APP_IDS, normalizePermissionKeys } from '@authportal/shared' export type AuthUser = { id: string @@ -68,7 +68,9 @@ export function loadAuthUser( user: UserRow, ): AuthUser { const apps = getUserApps(request.server.db, user.id) - const permissions = getUserPermissions(request.server.db, user.id) + const permissions = normalizePermissionKeys( + getUserPermissions(request.server.db, user.id), + ) return { id: user.id, email: user.email, diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 0307d15..caf070a 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -18,6 +18,7 @@ import { allPermissionKeys, appSwitcherConfigSchema, createUserRequestSchema, + normalizePermissionKeys, patchUserRequestSchema, putUserAccessRequestSchema, type AdminUser, @@ -34,7 +35,7 @@ function mapUser( const apps = getUserApps(db, user.id).filter((a): a is AppId => (APP_IDS as readonly string[]).includes(a), ) - const permissions = getUserPermissions(db, user.id) + const permissions = normalizePermissionKeys(getUserPermissions(db, user.id)) return { id: user.id, email: user.email, @@ -93,12 +94,13 @@ export async function adminRoutes(app: FastifyInstance): Promise { }) } const data = parsed.data + const permissions = normalizePermissionKeys(data.permissions) if (getUserByEmail(app.db, data.email)) { return reply.status(409).send({ error: { code: 'CONFLICT', message: 'Email уже занят' }, }) } - const accessError = validateAccess(data.apps, data.permissions) + const accessError = validateAccess(data.apps, permissions) if (accessError) { return reply.status(400).send({ error: { code: 'VALIDATION_ERROR', message: accessError }, @@ -112,7 +114,7 @@ export async function adminRoutes(app: FastifyInstance): Promise { passwordHash, isAdmin: data.is_admin, }) - setUserAccess(app.db, user.id, data.apps, data.permissions) + setUserAccess(app.db, user.id, data.apps, permissions) return reply.status(201).send(mapUser(app.db, getUserById(app.db, user.id)!)) }) @@ -190,10 +192,8 @@ export async function adminRoutes(app: FastifyInstance): Promise { error: { code: 'NOT_FOUND', message: 'Пользователь не найден' }, }) } - const accessError = validateAccess( - parsed.data.apps, - parsed.data.permissions, - ) + const permissions = normalizePermissionKeys(parsed.data.permissions) + const accessError = validateAccess(parsed.data.apps, permissions) if (accessError) { return reply.status(400).send({ error: { code: 'VALIDATION_ERROR', message: accessError }, @@ -203,7 +203,7 @@ export async function adminRoutes(app: FastifyInstance): Promise { app.db, request.params.id, parsed.data.apps, - parsed.data.permissions, + permissions, ) return mapUser(app.db, getUserById(app.db, request.params.id)!) }, diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 4b9d8c2..34a239f 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -5,6 +5,7 @@ import { PERMISSION_CATALOG, appsMetaFromSwitcher, loginRequestSchema, + normalizePermissionKeys, type LoginResponse, } from '@authportal/shared' import { @@ -52,7 +53,9 @@ export async function authRoutes(app: FastifyInstance): Promise { } const apps = getUserApps(app.db, user.id) - const permissions = getUserPermissions(app.db, user.id) + const permissions = normalizePermissionKeys( + getUserPermissions(app.db, user.id), + ) const me = toMe(user, apps, permissions) const expiresAt = new Date( diff --git a/packages/shared/src/contracts/auth.ts b/packages/shared/src/contracts/auth.ts index a5c4596..47778cb 100644 --- a/packages/shared/src/contracts/auth.ts +++ b/packages/shared/src/contracts/auth.ts @@ -244,6 +244,32 @@ export function allPermissionKeys(): string[] { return keys } +/** Old bgp section ids → current catalog (peers/apply/settings renamed). */ +const LEGACY_PERMISSION_MAP: Record = { + 'bgp:peers:read': 'bgp:network:read', + 'bgp:peers:write': 'bgp:network:write', + 'bgp:apply:write': 'bgp:operations:admin', + 'bgp:settings:admin': 'bgp:tenant_settings:admin', +} + +/** + * Map legacy keys and drop anything not in the current catalog. + * Safe for JWT issue and admin save after catalog changes. + */ +export function normalizePermissionKeys( + permissions: readonly string[], +): string[] { + const allowed = new Set(allPermissionKeys()) + const out = new Set() + for (const raw of permissions) { + const mapped = LEGACY_PERMISSION_MAP[raw] ?? raw + if (allowed.has(mapped)) { + out.add(mapped) + } + } + return [...out].sort() +} + export const permissionKeySchema = z .string() .regex(/^[a-z]+:[a-z0-9_]+:(read|write|admin)$/)