387 lines
11 KiB
TypeScript
387 lines
11 KiB
TypeScript
/**
|
|
* 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 (
|
|
* <div>
|
|
* {canCreate && <Button>Neu</Button>}
|
|
* {contracts.map(c => (
|
|
* <Row key={c.id}>
|
|
* {canUpdate(c) && <EditButton />}
|
|
* {canDelete(c) && <DeleteButton />}
|
|
* </Row>
|
|
* ))}
|
|
* </div>
|
|
* );
|
|
* }
|
|
* ```
|
|
*/
|
|
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 (
|
|
* <nav>
|
|
* {canViewContracts && <NavLink to="contracts">Verträge</NavLink>}
|
|
* </nav>
|
|
* );
|
|
* }
|
|
* ```
|
|
*
|
|
* 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<string, boolean> {
|
|
const { instance, featureCode } = useCurrentInstance();
|
|
|
|
return useMemo(() => {
|
|
const result: Record<string, boolean> = {};
|
|
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 (
|
|
* <form>
|
|
* {canRead && (
|
|
* <TextField
|
|
* name="salary"
|
|
* disabled={!canWrite}
|
|
* />
|
|
* )}
|
|
* </form>
|
|
* );
|
|
* }
|
|
* ```
|
|
*/
|
|
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
|
|
* <PermissionGate table="TrusteeContract" action="create">
|
|
* <Button>Neuer Vertrag</Button>
|
|
* </PermissionGate>
|
|
*
|
|
* <PermissionGate view="trustee-admin" fallback={<AccessDenied />}>
|
|
* <AdminPanel />
|
|
* </PermissionGate>
|
|
* ```
|
|
*/
|
|
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}</>;
|
|
}
|