src/services/permissions.ts
Rama main · réplica de lectura del repositorio original.
/**
* RBAC Permission System - Frontend
*
* This module provides the frontend interface to the backend RBAC permission system.
* It calls the SQL function rbac_check_permission(), backed by role_bindings.
*
* Usage:
* import { checkPermissions } from '~/services/permissions'
*
* // Check app-level permission
* const canUpload = await checkPermissions('app.upload_bundle', { appId: 'com.example.app' })
*
* // Check org-level permission
* const canInvite = await checkPermissions('org.invite_user', { orgId })
*
* // Check channel-level permission (orgId and appId are auto-derived by backend)
* const canPromote = await checkPermissions('channel.promote_bundle', { channelId: 123 })
*/
import type { Database } from '~/types/database.types'
import { useConsole } from '~/services/console'
import { useMainStore } from '~/stores/main'
/**
* All available RBAC permissions.
* These match exactly the keys in the public.permissions table.
*/
export type Permission
// Org permissions
= | 'org.read'
| 'org.update_settings'
| 'org.read_members'
| 'org.invite_user'
| 'org.update_user_roles'
| 'org.read_billing'
| 'org.update_billing'
| 'org.read_invoices'
| 'org.read_audit'
| 'org.read_billing_audit'
| 'org.create_app'
| 'org.manage_apikeys'
// App permissions
| 'app.read'
| 'app.update_settings'
| 'app.delete'
| 'app.read_bundles'
| 'app.upload_bundle'
| 'app.create_channel'
| 'app.read_channels'
| 'app.read_logs'
| 'app.manage_notifications'
| 'app.manage_devices'
| 'app.read_devices'
| 'app.build_native'
| 'app.read_audit'
| 'app.update_user_roles'
| 'app.manage_apikeys'
| 'app.transfer'
// Bundle permissions
| 'bundle.delete'
// Channel permissions
| 'channel.read'
| 'channel.update_settings'
| 'channel.delete'
| 'channel.read_history'
| 'channel.promote_bundle'
| 'channel.rollback_bundle'
| 'channel.manage_forced_devices'
| 'channel.read_forced_devices'
| 'channel.read_audit'
/**
* Scope identifiers for permission checks.
* At least one must be provided. More specific scopes (channelId) will auto-derive
* parent scopes (appId, orgId) if not explicitly provided by the backend.
*/
export interface PermissionScope {
orgId?: string
appId?: string
channelId?: number
}
export type PermissionCheckMode = 'all' | 'any'
export interface CheckPermissionsOptions {
mode?: PermissionCheckMode
}
/**
* Low-level single-permission check (RPC wrapper).
* Prefer checkPermissions() for new usage.
*/
export async function hasPermission(
permission: Permission,
scope: PermissionScope,
): Promise<boolean> {
const mainStore = useMainStore()
const capgo = useConsole()
// Get current user ID
const userId = mainStore.user?.id
if (!userId) {
console.warn('[hasPermission] No user ID found')
return false
}
try {
const { data, error } = await capgo.rpc('rbac_check_permission', {
p_permission_key: permission,
p_org_id: scope.orgId ?? undefined,
p_app_id: scope.appId ?? undefined,
p_channel_id: scope.channelId ?? undefined,
})
if (error) {
console.error('[hasPermission] RPC error:', error)
return false
}
return data === true
}
catch (err) {
console.error('[hasPermission] Exception:', err)
return false
}
}
/**
* Main permission check function.
*
* Calls the SQL function rbac_check_permission(), backed by role_bindings.
*
* The backend will:
* 1. Auto-derive parent scopes (orgId from appId, appId from channelId) if needed
* 2. Check role_bindings → roles → role_permissions → permissions
*
* @param permissions - A permission key or a list of permission keys
* @param scope - Scope identifiers. Parent scopes are auto-derived by the backend.
* @param options - Configure ALL/ANY behavior (default: ALL)
* @returns Promise<boolean> - true if the permission check passes, false otherwise
*
* @example
* // Check if user can upload bundles to an app
* if (await checkPermissions('app.upload_bundle', { appId: 'com.example.app' })) {
* // Show upload button
* }
*
* @example
* // Check if user can invite members to org
* if (await checkPermissions('org.invite_user', { orgId })) {
* // Show invite button
* }
*
* @example
* // Check channel permission (backend will auto-derive appId and orgId)
* if (await checkPermissions('channel.promote_bundle', { channelId: 123 })) {
* // Allow bundle promotion
* }
*
* @example
* // Check multiple permissions (ALL)
* if (await checkPermissions(['app.update_settings', 'app.delete'], { appId })) {
* // Show dangerous actions
* }
*
* @example
* // Check multiple permissions (ANY)
* if (await checkPermissions(['org.read_billing', 'org.read_invoices'], { orgId }, { mode: 'any' })) {
* // Show billing read-only UI
* }
*/
export async function checkPermissions(
permissions: Permission | Permission[],
scope: PermissionScope,
options: CheckPermissionsOptions = {},
): Promise<boolean> {
const perms = Array.isArray(permissions) ? permissions : [permissions]
if (perms.length === 0)
return false
const mode = options.mode ?? 'all'
if (mode === 'any') {
for (const perm of perms) {
if (await hasPermission(perm, scope))
return true
}
return false
}
for (const perm of perms) {
if (!(await hasPermission(perm, scope)))
return false
}
return true
}
/**
* Check whether a SPECIFIC user holds a permission in a scope.
*
* Unlike checkPermissions() (which checks the current user), this resolves another
* member's access. It is used to surface "who can grant you access" when the current
* user is blocked, so we list the people who actually hold the required permission.
*/
export async function userHasPermission(
permission: Permission,
userId: string,
scope: PermissionScope,
): Promise<boolean> {
const capgo = useConsole()
if (!userId || !scope.orgId)
return false
try {
// p_app_id / p_channel_id are nullable in SQL for org-scoped checks, but the
// generated types mark them required; pass null explicitly for an org-only check.
const { data, error } = await capgo.rpc('rbac_check_permission_direct', {
p_permission_key: permission,
p_user_id: userId,
p_org_id: scope.orgId,
p_app_id: scope.appId ?? null,
p_channel_id: scope.channelId ?? null,
} as unknown as Database['public']['Functions']['rbac_check_permission_direct']['Args'])
if (error) {
console.error('[userHasPermission] RPC error:', error)
return false
}
return data === true
}
catch (err) {
console.error('[userHasPermission] Exception:', err)
return false
}
}