Introduces a capability layer on top of existing roles that controls feature visibility and access. Capabilities follow an area.feature.action taxonomy (~35 capabilities) with sensible defaults per role. Tenant admins can customize via grant/revoke overrides stored in org settings JSONB. Key changes: - Add vice_president role to DB schema - Backend: capability constants, resolution logic, CapabilityGuard (global), @RequireCapability decorator on all 16 tenant controllers - Frontend: permission hooks (useCanEdit, useHasCapability), CapabilityGate component, sidebar filtering by capability, all 17 pages migrated from useIsReadOnly to capability-based checks - New admin UI: /settings/permissions matrix page for per-tenant role customization with grant/revoke delta model - GET /organizations/my-capabilities endpoint for capability refresh - Validation of permissionOverrides in settings updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
58 lines
1.4 KiB
TypeScript
58 lines
1.4 KiB
TypeScript
import { ALL_CAPABILITIES } from './capabilities';
|
|
import { DEFAULT_ROLE_CAPABILITIES } from './default-role-capabilities';
|
|
|
|
export interface PermissionOverrides {
|
|
[role: string]: {
|
|
grant?: string[];
|
|
revoke?: string[];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolve effective capabilities for a role, applying tenant overrides.
|
|
*
|
|
* 1. Start with default capabilities for the role
|
|
* 2. Add any granted capabilities from overrides
|
|
* 3. Remove any revoked capabilities from overrides
|
|
*
|
|
* Unknown capabilities in grant/revoke are silently ignored (they may
|
|
* come from an older version of the overrides).
|
|
*/
|
|
export function resolveCapabilities(
|
|
role: string,
|
|
overrides?: PermissionOverrides | null,
|
|
): Set<string> {
|
|
const defaults = DEFAULT_ROLE_CAPABILITIES[role] || [];
|
|
const result = new Set<string>(defaults);
|
|
|
|
if (overrides && overrides[role]) {
|
|
const roleOverride = overrides[role];
|
|
|
|
if (roleOverride.grant) {
|
|
for (const cap of roleOverride.grant) {
|
|
if (ALL_CAPABILITIES.has(cap)) {
|
|
result.add(cap);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (roleOverride.revoke) {
|
|
for (const cap of roleOverride.revoke) {
|
|
result.delete(cap);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Convenience: resolve to a sorted array (for API responses).
|
|
*/
|
|
export function resolveCapabilitiesArray(
|
|
role: string,
|
|
overrides?: PermissionOverrides | null,
|
|
): string[] {
|
|
return Array.from(resolveCapabilities(role, overrides)).sort();
|
|
}
|