export type Scope = 'none' | 'own' | 'all' export const SCOPE_RANK: Record = { none: 0, own: 1, all: 2 } export function isScope(value: unknown): value is Scope { return typeof value === 'string' && Object.keys(SCOPE_RANK).includes(value) } export interface Grant { key: string scope: Scope } export interface RoleRef { id: number name: string built_in: boolean locked: boolean } export interface AssignedBy { id: number name: string } export interface Access { role: RoleRef permissions: Grant[] roleAssignedBy?: AssignedBy | null } export function parseRoleAssignedBy(value: unknown): AssignedBy | null { if (typeof value !== 'object' || value === null) { return null } const { id, name } = value as { id?: unknown; name?: unknown } return typeof id === 'number' && typeof name === 'string' && name !== '' ? { id, name } : null } export interface CanOptions { ownerId?: number typeKey?: string scope?: 'own' | 'all' } export interface ScopeOptions { typeKey?: string } const TYPE_SEPARATOR = ':' export function formatGrantKey(key: string, typeKey?: string) { return typeKey === undefined ? key : `${key}${TYPE_SEPARATOR}${typeKey}` } export function parseGrantKey(key: string): { base: string; typeKey?: string } { const at = key.indexOf(TYPE_SEPARATOR) if (at === -1) { return { base: key } } return { base: key.slice(0, at), typeKey: key.slice(at + TYPE_SEPARATOR.length) } } function scopeOfGrant(grants: readonly Grant[], key: string): Scope | undefined { return grants.find((grant) => grant.key === key)?.scope } export function effectiveScope( grants: readonly Grant[], key: string, options: ScopeOptions = {}, ): Scope { if (options.typeKey) { const typed = scopeOfGrant(grants, formatGrantKey(key, options.typeKey)) if (typed !== undefined) { return typed } } return scopeOfGrant(grants, key) ?? 'none' } export function allows( grants: readonly Grant[], userId: number | undefined, key: string, options: CanOptions = {}, ): boolean { const scope = effectiveScope(grants, key, options) if (scope === 'none') { return false } if (scope === 'all') { return true } if (options.scope === 'all') { return false } if (options.ownerId === undefined) { return true } return userId !== undefined && options.ownerId !== 0 && options.ownerId === userId }