/** * Instance Permission Hooks * * Hooks für Berechtigungsprüfungen basierend auf der aktuellen Feature-Instanz. * Die Berechtigungen werden summarisch pro Instanz geladen (kein einzelner API-Call pro Check). */ import { useMemo } from 'react'; import { useCurrentInstance } from './useCurrentInstance'; import type { TablePermission, FieldPermission, AccessLevel, InstancePermissions, } from '../types/mandate'; import { canAccessRecord, hasAccess } from '../types/mandate'; // ============================================================================= // DEFAULT PERMISSIONS (Kein Zugriff) // ============================================================================= const NO_ACCESS_TABLE: TablePermission = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; // ============================================================================= // TABLE PERMISSION HOOKS // ============================================================================= /** * Hook für Tabellen-Berechtigungen * * Verwendung: * ```tsx * function ContractList() { * const { canCreate, canUpdate, canDelete, read } = useTablePermission('TrusteeContract'); * * return ( *
* {canCreate && } * {contracts.map(c => ( * * {canUpdate(c) && } * {canDelete(c) && } * * ))} *
* ); * } * ``` */ export function useTablePermission(tableName: string) { const { instance } = useCurrentInstance(); const permission = useMemo((): TablePermission => { if (!instance?.permissions?.tables) { return NO_ACCESS_TABLE; } return instance.permissions.tables[tableName] ?? NO_ACCESS_TABLE; }, [instance, tableName]); // Kontext für Record-basierte Prüfungen const userId = ''; // TODO: Aus Auth-Store holen return { // Raw permission levels view: permission.view, read: permission.read, create: permission.create, update: permission.update, delete: permission.delete, // Convenience Booleans canView: permission.view, canRead: hasAccess(permission.read), canCreate: hasAccess(permission.create), canUpdate: hasAccess(permission.update), canDelete: hasAccess(permission.delete), // Record-basierte Prüfungen canReadRecord: (record: { _createdBy?: string }) => canAccessRecord(permission.read, record, userId), canUpdateRecord: (record: { _createdBy?: string }) => canAccessRecord(permission.update, record, userId), canDeleteRecord: (record: { _createdBy?: string }) => canAccessRecord(permission.delete, record, userId), }; } /** * Vereinfachter Hook - prüft nur ob Tabelle sichtbar ist */ export function useCanViewTable(tableName: string): boolean { const { canView } = useTablePermission(tableName); return canView; } // ============================================================================= // VIEW PERMISSION HOOKS // ============================================================================= /** * Hook für View-Berechtigungen (Navigation) * * Verwendung: * ```tsx * function Navigation() { * const canViewContracts = useCanViewFeatureView('trustee-contracts'); * * return ( * * ); * } * ``` * * Supports both legacy format (e.g., "trustee-dashboard") and * fully qualified objectKey format (e.g., "ui.feature.trustee.dashboard") */ export function useCanViewFeatureView(viewCode: string): boolean { const { instance, featureCode } = useCurrentInstance(); if (!instance?.permissions?.views) { // DEBUG: Log for chatbot if (featureCode === 'chatbot') { console.log('🔍 [DEBUG] useCanViewFeatureView: No views permissions', { viewCode, featureCode, instanceId: instance?.id, hasPermissions: !!instance?.permissions, hasViews: !!instance?.permissions?.views, }); } return false; } const views = instance.permissions.views; // DEBUG: Log for chatbot if (featureCode === 'chatbot') { const parts = viewCode.split('-'); const viewName = parts.length >= 2 ? parts.slice(1).join('-') : ''; const fullObjectKey = `ui.feature.${featureCode}.${viewName}`; console.log('🔍 [DEBUG] useCanViewFeatureView: Checking permissions', { viewCode, featureCode, viewName, fullObjectKey, instanceId: instance.id, viewKeys: Object.keys(views), hasWildcard: !!views["_all"], hasLegacyView: !!views[viewCode], hasFullObjectKey: !!views[fullObjectKey], wildcardValue: views["_all"], legacyValue: views[viewCode], fullObjectKeyValue: views[fullObjectKey], }); } // Check for wildcard "_all" permission first (item=None in backend = all views) if (views["_all"]) { return true; } // Check legacy format directly (e.g., "trustee-dashboard") if (views[viewCode]) { return true; } // Check fully qualified objectKey format (e.g., "ui.feature.trustee.dashboard") // Convert viewCode "trustee-dashboard" to "ui.feature.trustee.dashboard" const parts = viewCode.split('-'); if (parts.length >= 2 && featureCode) { const viewName = parts.slice(1).join('-'); // e.g., "dashboard" or "position-documents" const fullObjectKey = `ui.feature.${featureCode}.${viewName}`; if (views[fullObjectKey]) { return true; } } return false; } /** * Hook für mehrere View-Berechtigungen gleichzeitig * Supports both legacy format and fully qualified objectKey format */ export function useViewPermissions(viewCodes: string[]): Record { const { instance, featureCode } = useCurrentInstance(); return useMemo(() => { const result: Record = {}; const views = instance?.permissions?.views; if (!views) { viewCodes.forEach(code => { result[code] = false; }); return result; } // Check for wildcard permission const hasAllViews = views["_all"] ?? false; viewCodes.forEach(code => { if (hasAllViews) { result[code] = true; return; } // Check legacy format if (views[code]) { result[code] = true; return; } // Check fully qualified objectKey format const parts = code.split('-'); if (parts.length >= 2 && featureCode) { const viewName = parts.slice(1).join('-'); const fullObjectKey = `ui.feature.${featureCode}.${viewName}`; if (views[fullObjectKey]) { result[code] = true; return; } } result[code] = false; }); return result; }, [instance, featureCode, viewCodes]); } // ============================================================================= // FIELD PERMISSION HOOKS // ============================================================================= /** * Hook für Feld-Berechtigungen * * Verwendung: * ```tsx * function ContractForm() { * const { canRead, canWrite } = useFieldPermission('TrusteeContract', 'salary'); * * return ( *
* {canRead && ( * * )} * * ); * } * ``` */ export function useFieldPermission(tableName: string, fieldName: string): FieldPermission { const { instance } = useCurrentInstance(); return useMemo(() => { const fieldPermissions = instance?.permissions?.fields?.[tableName]; if (!fieldPermissions) { // Wenn keine Feld-Level Einschränkungen, erlaube alles return { read: true, write: true }; } return fieldPermissions[fieldName] ?? { read: true, write: true }; }, [instance, tableName, fieldName]); } // ============================================================================= // GENERIC PERMISSION CHECK // ============================================================================= /** * Generischer Hook für beliebige Berechtigungsprüfungen */ export function useInstancePermissions(): InstancePermissions | undefined { const { instance } = useCurrentInstance(); return instance?.permissions; } /** * Hook der prüft ob ein Record bearbeitet werden darf * Kombiniert Tabellen-Permission mit Record-Owner-Check */ export function useCanEditRecord( tableName: string, record: { _createdBy?: string } | undefined, userId: string ): boolean { const { update } = useTablePermission(tableName); if (!record) return false; return canAccessRecord(update, record, userId); } /** * Hook der prüft ob ein Record gelöscht werden darf */ export function useCanDeleteRecord( tableName: string, record: { _createdBy?: string } | undefined, userId: string ): boolean { const { delete: deleteLevel } = useTablePermission(tableName); if (!record) return false; return canAccessRecord(deleteLevel, record, userId); } // ============================================================================= // PERMISSION GATE COMPONENT // ============================================================================= interface PermissionGateProps { table?: string; view?: string; action?: 'view' | 'read' | 'create' | 'update' | 'delete'; record?: { _createdBy?: string }; children: React.ReactNode; fallback?: React.ReactNode; } /** * Komponente für bedingte Anzeige basierend auf Berechtigungen * * Verwendung: * ```tsx * * * * * }> * * * ``` */ export function PermissionGate({ table, view, action = 'view', record, children, fallback = null, }: PermissionGateProps): React.ReactElement | null { const { instance } = useCurrentInstance(); const userId = ''; // TODO: Aus Auth-Store holen let hasPermission = false; if (view) { // View-basierte Prüfung hasPermission = instance?.permissions?.views?.[view] ?? false; } else if (table) { // Tabellen-basierte Prüfung const tablePermission = instance?.permissions?.tables?.[table]; if (!tablePermission) { hasPermission = false; } else if (action === 'view') { hasPermission = tablePermission.view; } else { const level = tablePermission[action] as AccessLevel; if (record) { hasPermission = canAccessRecord(level, record, userId); } else { hasPermission = hasAccess(level); } } } return hasPermission ? <>{children} : <>{fallback}; }