docu rbac extended
This commit is contained in:
parent
b94695e70e
commit
34145bfe38
2 changed files with 1844 additions and 0 deletions
863
concepts/Navigation-API-Konzept.md
Normal file
863
concepts/Navigation-API-Konzept.md
Normal file
|
|
@ -0,0 +1,863 @@
|
|||
# Navigation API Konzept
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Dokument beschreibt das Konzept für die Navigation-API, welche dem UI alle notwendigen Informationen für das Rendering der Navigation liefert. Die API ist die **Single Source of Truth** für die Navigationsstruktur.
|
||||
|
||||
## Grundprinzipien
|
||||
|
||||
### 1. Backend liefert, UI rendert
|
||||
- Das **Gateway** bestimmt, welche Navigationselemente ein User sehen darf
|
||||
- Das **UI** rendert nur das, was es vom Gateway erhält
|
||||
- Keine Permission-Logik im UI für Navigation
|
||||
|
||||
### 2. Trennung von Daten und Darstellung
|
||||
- Die API liefert **keine Style-Elemente** (Icons, CSS-Klassen, etc.)
|
||||
- Die API liefert **UI-Komponenten-Codes**, die das UI auf seine Komponenten mappt
|
||||
- Verschiedene UIs können dieselben Daten unterschiedlich darstellen
|
||||
|
||||
### 3. Nur sichtbare Elemente
|
||||
- Elemente ohne Zugriffsberechtigung werden **nicht** in den Baum aufgenommen
|
||||
- Kein `hasAccess: false` - wenn kein Zugriff, dann nicht im Response
|
||||
|
||||
### 4. Fehlertoleranz
|
||||
- Wenn ein Code im UI nicht gemappt werden kann → Fehlertext im Nav-Tree
|
||||
- Synchronisationsprobleme zwischen Gateway und UI werden sofort sichtbar
|
||||
|
||||
### 5. Personalisierbare Sortierung
|
||||
- Jedes Element hat eine `order` Nummer für die Sortierung
|
||||
- User können die Reihenfolge anpassen durch User-Settings
|
||||
- Gateway liefert bereits sortiert (Default + User-Override)
|
||||
|
||||
---
|
||||
|
||||
## API Response Struktur
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
GET /api/navigation
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "de",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "static",
|
||||
"id": "system",
|
||||
"title": "SYSTEM",
|
||||
"order": 10,
|
||||
"items": [...]
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"id": "workflows",
|
||||
"title": "WORKFLOWS",
|
||||
"order": 20,
|
||||
"items": [...]
|
||||
},
|
||||
{
|
||||
"type": "dynamic",
|
||||
"id": "features",
|
||||
"title": "FEATURES",
|
||||
"order": 100,
|
||||
"mandates": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Block-Typen
|
||||
|
||||
### Static Block
|
||||
|
||||
Für System-Seiten, die nicht an Feature-Instanzen gebunden sind.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "static",
|
||||
"id": "system",
|
||||
"title": "SYSTEM",
|
||||
"order": 10,
|
||||
"items": [
|
||||
{
|
||||
"uiComponent": "page.system.home",
|
||||
"uiLabel": "Übersicht",
|
||||
"uiPath": "/",
|
||||
"order": 10,
|
||||
"objectKey": "ui.system.home"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.system.settings",
|
||||
"uiLabel": "Einstellungen",
|
||||
"uiPath": "/settings",
|
||||
"order": 20,
|
||||
"objectKey": "ui.system.settings"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Felder pro Item:**
|
||||
| Feld | Beschreibung |
|
||||
|------|--------------|
|
||||
| `uiComponent` | Eindeutiger Code für UI-Mapping (z.B. `page.system.home`) |
|
||||
| `uiLabel` | Anzeigetext (bereits in der gewählten Sprache) |
|
||||
| `uiPath` | URL-Pfad für Navigation |
|
||||
| `order` | Sortierreihenfolge (Default oder User-Override) |
|
||||
| `objectKey` | Vollqualifizierter RBAC-Objektname (für Debugging/Referenz) |
|
||||
|
||||
### Dynamic Block
|
||||
|
||||
Für Feature-Instanzen, gruppiert nach Mandanten.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "dynamic",
|
||||
"id": "features",
|
||||
"title": "FEATURES",
|
||||
"order": 100,
|
||||
"mandates": [
|
||||
{
|
||||
"id": "5ce04434-c4ce-4269-9861-19ff7ebc2a1d",
|
||||
"uiLabel": "SOHA Treuhand AG",
|
||||
"order": 10,
|
||||
"features": [
|
||||
{
|
||||
"uiComponent": "feature.trustee",
|
||||
"uiLabel": "Treuhand",
|
||||
"order": 10,
|
||||
"instances": [
|
||||
{
|
||||
"id": "2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0",
|
||||
"uiLabel": "ValueOn AG 2026",
|
||||
"order": 10,
|
||||
"views": [
|
||||
{
|
||||
"uiComponent": "page.feature.trustee.dashboard",
|
||||
"uiLabel": "Übersicht",
|
||||
"uiPath": "/mandates/5ce04434.../trustee/2fc48f66.../dashboard",
|
||||
"order": 10,
|
||||
"objectKey": "ui.feature.trustee.dashboard"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.feature.trustee.positions",
|
||||
"uiLabel": "Positionen",
|
||||
"uiPath": "/mandates/5ce04434.../trustee/2fc48f66.../positions",
|
||||
"order": 20,
|
||||
"objectKey": "ui.feature.trustee.positions"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Hinweis:** Der `dynamic` Block ist nur vorhanden, wenn der User mindestens eine Feature-Instanz hat.
|
||||
|
||||
**Sortierung auf allen Ebenen:**
|
||||
- Mandanten: `mandates[].order`
|
||||
- Features: `features[].order`
|
||||
- Instanzen: `instances[].order`
|
||||
- Views: `views[].order`
|
||||
|
||||
---
|
||||
|
||||
## User-Personalisierung (Order Override)
|
||||
|
||||
### Konzept
|
||||
|
||||
User können die Reihenfolge der Navigation anpassen. Die Einstellungen werden in den User-Settings gespeichert.
|
||||
|
||||
### User Settings Struktur
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": "user-123",
|
||||
"settings": {
|
||||
"navOrder": {
|
||||
"page.system.home": 10,
|
||||
"page.system.prompts": 5,
|
||||
"page.admin.users": 100,
|
||||
"feature.trustee": 20,
|
||||
"feature.realestate": 10,
|
||||
"mandate.5ce04434-...": 5,
|
||||
"instance.2fc48f66-...": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Merge-Logik im Gateway
|
||||
|
||||
```
|
||||
Effektive Order = User-Override ?? Default-Order ?? 50
|
||||
```
|
||||
|
||||
1. Gateway lädt Default-Order aus Konfiguration
|
||||
2. Gateway lädt User-Settings (falls vorhanden)
|
||||
3. Für jedes Element: User-Order überschreibt Default
|
||||
4. Sortierung erfolgt im Gateway, UI erhält bereits sortiert
|
||||
|
||||
### Sortier-Priorität
|
||||
|
||||
| Ebene | Key für navOrder | Beispiel |
|
||||
|-------|------------------|----------|
|
||||
| Block | Block-ID | `"system"`, `"workflows"`, `"features"` |
|
||||
| Static Item | uiComponent | `"page.system.home"` |
|
||||
| Mandate | `mandate.{id}` | `"mandate.5ce04434-..."` |
|
||||
| Feature | uiComponent | `"feature.trustee"` |
|
||||
| Instance | `instance.{id}` | `"instance.2fc48f66-..."` |
|
||||
| View | uiComponent | `"page.feature.trustee.dashboard"` |
|
||||
|
||||
### API für Order-Update
|
||||
|
||||
```
|
||||
PUT /api/user/settings/navOrder
|
||||
Body: { "page.system.prompts": 5, "feature.trustee": 20 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code-Konvention
|
||||
|
||||
### Statische Seiten
|
||||
```
|
||||
page.system.<page-name>
|
||||
page.admin.<page-name>
|
||||
```
|
||||
|
||||
Beispiele:
|
||||
- `page.system.home`
|
||||
- `page.system.settings`
|
||||
- `page.system.prompts`
|
||||
- `page.admin.users`
|
||||
- `page.admin.mandates`
|
||||
|
||||
### Feature-Seiten
|
||||
```
|
||||
page.feature.<feature-code>.<view-name>
|
||||
```
|
||||
|
||||
Beispiele:
|
||||
- `page.feature.trustee.dashboard`
|
||||
- `page.feature.trustee.positions`
|
||||
- `page.feature.trustee.instance-roles`
|
||||
- `page.feature.realestate.projects`
|
||||
|
||||
### Feature-Codes (für Gruppierung)
|
||||
```
|
||||
feature.<feature-code>
|
||||
```
|
||||
|
||||
Beispiele:
|
||||
- `feature.trustee`
|
||||
- `feature.realestate`
|
||||
- `feature.chatworkflow`
|
||||
|
||||
---
|
||||
|
||||
## ObjectKey-Konvention (RBAC)
|
||||
|
||||
Die `objectKey` werden für Access Rules verwendet und sind vollqualifiziert:
|
||||
|
||||
### UI Objekte
|
||||
```
|
||||
ui.system.<name> → System-Seiten
|
||||
ui.admin.<name> → Admin-Seiten
|
||||
ui.feature.<code>.<view> → Feature-Views
|
||||
```
|
||||
|
||||
### DATA Objekte
|
||||
```
|
||||
data.system.<table> → System-Tabellen
|
||||
data.feature.<code>.<table> → Feature-Tabellen
|
||||
```
|
||||
|
||||
### RESOURCE Objekte
|
||||
```
|
||||
resource.system.<action> → System-Aktionen
|
||||
resource.feature.<code>.<action> → Feature-Aktionen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Mapping & Fehlerbehandlung
|
||||
|
||||
### UI Code Registry
|
||||
|
||||
Das UI definiert ein Mapping von Codes zu Komponenten:
|
||||
|
||||
```typescript
|
||||
// frontend_nyla/src/config/pageRegistry.ts
|
||||
|
||||
export const PAGE_REGISTRY: Record<string, PageDefinition> = {
|
||||
// Static pages
|
||||
'page.system.home': {
|
||||
component: HomePage,
|
||||
icon: FaHome,
|
||||
layout: 'default',
|
||||
},
|
||||
'page.system.settings': {
|
||||
component: SettingsPage,
|
||||
icon: FaCog,
|
||||
layout: 'default',
|
||||
},
|
||||
'page.admin.users': {
|
||||
component: AdminUsersPage,
|
||||
icon: FaUsers,
|
||||
layout: 'admin',
|
||||
},
|
||||
|
||||
// Feature pages
|
||||
'page.feature.trustee.dashboard': {
|
||||
component: TrusteeDashboardView,
|
||||
icon: FaChartBar,
|
||||
layout: 'feature',
|
||||
},
|
||||
'page.feature.trustee.positions': {
|
||||
component: TrusteePositionsView,
|
||||
icon: FaListAlt,
|
||||
layout: 'feature',
|
||||
},
|
||||
// ...
|
||||
};
|
||||
|
||||
export const FEATURE_REGISTRY: Record<string, FeatureDefinition> = {
|
||||
'feature.trustee': {
|
||||
icon: FaBriefcase,
|
||||
color: '#e74c3c',
|
||||
},
|
||||
'feature.realestate': {
|
||||
icon: FaBuilding,
|
||||
color: '#3498db',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Fehlerbehandlung
|
||||
|
||||
Wenn ein `uiComponent` Code nicht gemappt werden kann:
|
||||
|
||||
```typescript
|
||||
function renderNavItem(item: NavItem) {
|
||||
const pageDef = PAGE_REGISTRY[item.uiComponent];
|
||||
|
||||
if (!pageDef) {
|
||||
// Fehler sichtbar machen!
|
||||
return (
|
||||
<NavError>
|
||||
⚠️ Unbekannter Code: {item.uiComponent}
|
||||
<small>ObjectKey: {item.objectKey}</small>
|
||||
</NavError>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink to={item.uiPath} icon={pageDef.icon}>
|
||||
{item.uiLabel}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Wichtig:** Fehler werden im Nav-Tree angezeigt, nicht versteckt! So werden Synchronisationsprobleme zwischen Gateway und UI sofort sichtbar.
|
||||
|
||||
### Sortierung im UI
|
||||
|
||||
Das UI sortiert die bereits vom Gateway sortierten Elemente NICHT neu. Die Sortierung erfolgt ausschliesslich im Gateway basierend auf:
|
||||
|
||||
1. Default-Order aus Konfiguration
|
||||
2. User-Override aus Settings (falls vorhanden)
|
||||
|
||||
```typescript
|
||||
// UI rendert in der Reihenfolge wie empfangen
|
||||
function renderBlock(block: Block) {
|
||||
// items sind bereits sortiert
|
||||
return block.items.map(item => renderNavItem(item));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementierungsschritte
|
||||
|
||||
### Phase 1: Gateway - Navigation Endpoint
|
||||
|
||||
1. **Neuer Endpoint** `/api/navigation` erstellen
|
||||
2. **Static Blocks** aus `NAVIGATION_SECTIONS` generieren
|
||||
3. **Dynamic Block** aus Feature-Instanzen generieren
|
||||
4. **Permission-Filter** anwenden (nur sichtbare Elemente)
|
||||
5. **uiComponent Codes** gemäß Konvention setzen
|
||||
6. **Default-Order** für alle Elemente setzen
|
||||
|
||||
### Phase 2: Gateway - Access Rules
|
||||
|
||||
1. **TEMPLATE_ROLES** auf vollqualifizierte `objectKey` umstellen
|
||||
2. **Permission-Check** auf `objectKey` umstellen
|
||||
3. **Migration** bestehender Access Rules (falls nötig)
|
||||
|
||||
### Phase 3: Gateway - User Settings
|
||||
|
||||
1. **User Settings Modell** erweitern mit `navOrder`
|
||||
2. **API Endpoint** `PUT /api/user/settings/navOrder` erstellen
|
||||
3. **Merge-Logik** implementieren (User-Override → Default)
|
||||
4. **Sortierung** vor Response durchführen
|
||||
|
||||
### Phase 4: Frontend
|
||||
|
||||
1. **PAGE_REGISTRY** erstellen mit uiComponent → Component Mapping
|
||||
2. **FEATURE_REGISTRY** erstellen mit Feature-Code → Style Mapping
|
||||
3. **useNavigation Hook** auf neuen Endpoint umstellen
|
||||
4. **MandateNavigation** refactoren für neue Datenstruktur
|
||||
5. **Fehlerhandling** für unbekannte uiComponents implementieren
|
||||
|
||||
### Phase 5: Frontend - Drag & Drop (Optional)
|
||||
|
||||
1. **Drag & Drop UI** für Nav-Tree Sortierung
|
||||
2. **API Call** bei Order-Änderung
|
||||
3. **Optimistic Update** für schnelle UX
|
||||
|
||||
### Phase 6: Cleanup
|
||||
|
||||
1. **FEATURE_REGISTRY** (alte Version in mandate.ts) entfernen
|
||||
2. **Hardcodierte Navigation** im Frontend entfernen
|
||||
3. **Tests** für Synchronisation Gateway ↔ UI
|
||||
|
||||
---
|
||||
|
||||
## Beispiel: Vollständige Response
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "de",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "static",
|
||||
"id": "system",
|
||||
"title": "SYSTEM",
|
||||
"order": 10,
|
||||
"items": [
|
||||
{
|
||||
"uiComponent": "page.system.home",
|
||||
"uiLabel": "Übersicht",
|
||||
"uiPath": "/",
|
||||
"order": 10,
|
||||
"objectKey": "ui.system.home"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.system.settings",
|
||||
"uiLabel": "Einstellungen",
|
||||
"uiPath": "/settings",
|
||||
"order": 20,
|
||||
"objectKey": "ui.system.settings"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"id": "workflows",
|
||||
"title": "WORKFLOWS",
|
||||
"order": 20,
|
||||
"items": [
|
||||
{
|
||||
"uiComponent": "page.system.playground",
|
||||
"uiLabel": "Chat Playground",
|
||||
"uiPath": "/workflows/playground",
|
||||
"order": 10,
|
||||
"objectKey": "ui.system.playground"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.system.chats",
|
||||
"uiLabel": "Chats",
|
||||
"uiPath": "/workflows/list",
|
||||
"order": 20,
|
||||
"objectKey": "ui.system.chats"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"id": "basedata",
|
||||
"title": "BASISDATEN",
|
||||
"order": 30,
|
||||
"items": [
|
||||
{
|
||||
"uiComponent": "page.system.prompts",
|
||||
"uiLabel": "Prompts",
|
||||
"uiPath": "/basedata/prompts",
|
||||
"order": 10,
|
||||
"objectKey": "ui.system.prompts"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.system.files",
|
||||
"uiLabel": "Dateien",
|
||||
"uiPath": "/basedata/files",
|
||||
"order": 20,
|
||||
"objectKey": "ui.system.files"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "static",
|
||||
"id": "admin",
|
||||
"title": "ADMINISTRATION",
|
||||
"order": 200,
|
||||
"items": [
|
||||
{
|
||||
"uiComponent": "page.admin.users",
|
||||
"uiLabel": "Benutzer",
|
||||
"uiPath": "/admin/users",
|
||||
"order": 10,
|
||||
"objectKey": "ui.admin.users"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.admin.mandates",
|
||||
"uiLabel": "Mandanten",
|
||||
"uiPath": "/admin/mandates",
|
||||
"order": 20,
|
||||
"objectKey": "ui.admin.mandates"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dynamic",
|
||||
"id": "features",
|
||||
"title": "MEINE FEATURES",
|
||||
"order": 100,
|
||||
"mandates": [
|
||||
{
|
||||
"id": "5ce04434-c4ce-4269-9861-19ff7ebc2a1d",
|
||||
"uiLabel": "SOHA Treuhand AG",
|
||||
"order": 10,
|
||||
"features": [
|
||||
{
|
||||
"uiComponent": "feature.trustee",
|
||||
"uiLabel": "Treuhand",
|
||||
"order": 10,
|
||||
"instances": [
|
||||
{
|
||||
"id": "2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0",
|
||||
"uiLabel": "ValueOn AG 2026",
|
||||
"order": 10,
|
||||
"views": [
|
||||
{
|
||||
"uiComponent": "page.feature.trustee.dashboard",
|
||||
"uiLabel": "Übersicht",
|
||||
"uiPath": "/mandates/5ce04434-c4ce-4269-9861-19ff7ebc2a1d/trustee/2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0/dashboard",
|
||||
"order": 10,
|
||||
"objectKey": "ui.feature.trustee.dashboard"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.feature.trustee.positions",
|
||||
"uiLabel": "Positionen",
|
||||
"uiPath": "/mandates/5ce04434-c4ce-4269-9861-19ff7ebc2a1d/trustee/2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0/positions",
|
||||
"order": 20,
|
||||
"objectKey": "ui.feature.trustee.positions"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.feature.trustee.documents",
|
||||
"uiLabel": "Dokumente",
|
||||
"uiPath": "/mandates/5ce04434-c4ce-4269-9861-19ff7ebc2a1d/trustee/2fc48f66-ad1b-4581-aa87-6aaa1e7c16e0/documents",
|
||||
"order": 30,
|
||||
"objectKey": "ui.feature.trustee.documents"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "abc123-mandate-2",
|
||||
"uiLabel": "Partner AG",
|
||||
"order": 20,
|
||||
"features": [
|
||||
{
|
||||
"uiComponent": "feature.realestate",
|
||||
"uiLabel": "Immobilien",
|
||||
"order": 10,
|
||||
"instances": [
|
||||
{
|
||||
"id": "def456-instance",
|
||||
"uiLabel": "Objektstudien ZH",
|
||||
"order": 10,
|
||||
"views": [
|
||||
{
|
||||
"uiComponent": "page.feature.realestate.dashboard",
|
||||
"uiLabel": "Übersicht",
|
||||
"uiPath": "/mandates/abc123-mandate-2/realestate/def456-instance/dashboard",
|
||||
"order": 10,
|
||||
"objectKey": "ui.feature.realestate.dashboard"
|
||||
},
|
||||
{
|
||||
"uiComponent": "page.feature.realestate.projects",
|
||||
"uiLabel": "Projekte",
|
||||
"uiPath": "/mandates/abc123-mandate-2/realestate/def456-instance/projects",
|
||||
"order": 20,
|
||||
"objectKey": "ui.feature.realestate.projects"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vorteile dieses Ansatzes
|
||||
|
||||
1. **Single Source of Truth**: Gateway definiert, was sichtbar ist
|
||||
2. **Keine Permission-Logik im UI**: Alles was kommt, wird gerendert
|
||||
3. **Flexibles UI**: Verschiedene UIs können dieselben Daten anders darstellen
|
||||
4. **Fehlertoleranz**: Synchronisationsprobleme werden sofort sichtbar
|
||||
5. **Saubere Trennung**: Daten (Gateway) vs. Darstellung (UI)
|
||||
6. **Testbarkeit**: API-Response kann einfach getestet werden
|
||||
7. **Dokumentation**: Codes und ObjectKeys sind selbstdokumentierend
|
||||
8. **Personalisierung**: User können Navigation individuell sortieren
|
||||
9. **Konsistente Sortierung**: Gateway sortiert zentral, UI respektiert die Reihenfolge
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen (Beantwortet)
|
||||
|
||||
1. Soll der `dynamic` Block vor oder nach den `static` Blocks kommen? → **Diese initiale Reihenfolge: System, `<dynamic>`, workflows, basisdaten, migrate to features, administration**
|
||||
2. Braucht es eine Versionierung der API für Breaking Changes? → **Nein**
|
||||
3. Wie werden Feature-spezifische Sub-Navigationen behandelt (z.B. Tabs innerhalb einer View)? → **Tabs innerhalb einer View werden als separate Views behandelt**
|
||||
4. Soll der Default-Order-Abstand 10 sein (erlaubt Einfügen zwischen Elementen)? → **Ja**
|
||||
5. Soll die Order-Personalisierung auch für Admins pro Mandate/Feature-Instanz möglich sein (System-Default vs. User-Override)? → **Ja**
|
||||
|
||||
---
|
||||
|
||||
## Code-Analyse: IST-Zustand
|
||||
|
||||
### Gateway (Backend)
|
||||
|
||||
#### 1. `mainSystem.py` - Statische Navigation
|
||||
|
||||
```python
|
||||
# IST: Items haben id, label, path, icon, objectKey
|
||||
# NEU: Brauchen uiComponent, uiLabel, uiPath, order (ohne icon)
|
||||
NAVIGATION_SECTIONS = [
|
||||
{
|
||||
"id": "system",
|
||||
"order": 10, # ✓ Vorhanden
|
||||
"items": [
|
||||
{
|
||||
"id": "home",
|
||||
"objectKey": "ui.system.home", # ✓ Korrekt
|
||||
"label": {"en": "Home", "de": "Übersicht"},
|
||||
"icon": "FaHome", # ✗ Muss entfernt werden
|
||||
"path": "/",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Änderungen:**
|
||||
- `icon` entfernen (UI mappt selbst)
|
||||
- Item-Level `order` hinzufügen
|
||||
- Felder umbenennen für API Response
|
||||
|
||||
#### 2. `routeSystem.py` - Navigation Endpoint
|
||||
|
||||
```python
|
||||
# IST: GET /api/system/navigation
|
||||
# Gibt zurück: { "sections": [...], "language": "de" }
|
||||
|
||||
# NEU: GET /api/navigation
|
||||
# Soll zurückgeben: { "blocks": [...], "language": "de" }
|
||||
```
|
||||
|
||||
**Änderungen:**
|
||||
- Neuer kombinierter Endpoint
|
||||
- Static Blocks + Dynamic Block zusammenführen
|
||||
- Response-Struktur gemäss Konzept
|
||||
|
||||
#### 3. `mainTrustee.py` - Feature Views
|
||||
|
||||
```python
|
||||
# IST: TEMPLATE_ROLES verwendet Kurznamen
|
||||
TEMPLATE_ROLES = [
|
||||
{
|
||||
"roleLabel": "trustee-client",
|
||||
"accessRules": [
|
||||
{"context": "UI", "item": "dashboard", "view": True}, # ✗ Kurzname!
|
||||
{"context": "UI", "item": "positions", "view": True}, # ✗ Kurzname!
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# ABER: UI_OBJECTS verwendet vollqualifizierte Namen
|
||||
UI_OBJECTS = [
|
||||
{"objectKey": "ui.feature.trustee.dashboard", ...}, # ✓ Vollständig
|
||||
]
|
||||
```
|
||||
|
||||
**Inkonsistenz gefunden!** `item` in AccessRules muss `objectKey` entsprechen.
|
||||
|
||||
#### 4. `routeAdminFeatures.py` - Permission Logic
|
||||
|
||||
```python
|
||||
# IST: _deriveViewPermissions ist TRUSTEE-spezifisch!
|
||||
def _deriveViewPermissions(permissions):
|
||||
# Hard-coded: TrusteePosition → positions view
|
||||
# Hard-coded: TrusteeDocument → documents view
|
||||
```
|
||||
|
||||
**Problem:** Nicht generisch, funktioniert nur für Trustee.
|
||||
|
||||
### Frontend (UI)
|
||||
|
||||
#### 1. `MandateNavigation.tsx`
|
||||
|
||||
```typescript
|
||||
// IST: Zwei separate Datenquellen
|
||||
const { sections } = useNavigation(); // Static von API
|
||||
const mandates = useMandates(); // Dynamic von API
|
||||
|
||||
// IST: FEATURE_ICONS definiert im Frontend
|
||||
const FEATURE_ICONS = { trustee: <FaBriefcase /> };
|
||||
```
|
||||
|
||||
**Problem:** Navigation wird im Frontend zusammengebaut, nicht vom Backend.
|
||||
|
||||
#### 2. `mandate.ts` - FEATURE_REGISTRY
|
||||
|
||||
```typescript
|
||||
// IST: Doppelte Definition von Views!
|
||||
export const FEATURE_REGISTRY = {
|
||||
trustee: {
|
||||
views: [
|
||||
{ code: 'dashboard', label: {...}, path: 'dashboard' }, // ✗ Kurzname
|
||||
{ code: 'positions', label: {...}, path: 'positions' }, // ✗ Kurzname
|
||||
]
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Duplizierung:** Views sind in `mainTrustee.py` UND `mandate.ts` definiert!
|
||||
|
||||
---
|
||||
|
||||
## Impact-Analyse
|
||||
|
||||
### Phase 1: Gateway - Navigation Endpoint
|
||||
|
||||
| Datei | Änderungstyp | Aufwand | Breaking |
|
||||
|-------|--------------|---------|----------|
|
||||
| `routeSystem.py` | Neu/Refactor | Hoch | Ja |
|
||||
| `mainSystem.py` | Update | Mittel | Nein |
|
||||
| `routeAdminFeatures.py` | Integrieren | Hoch | Nein |
|
||||
|
||||
**Konkrete Aufgaben:**
|
||||
1. Neuen Endpoint `GET /api/navigation` erstellen
|
||||
2. Static Blocks aus `NAVIGATION_SECTIONS` generieren
|
||||
3. Dynamic Block aus `_getMyFeatureInstances`-Logik generieren
|
||||
4. Permission-Filter auf beide anwenden
|
||||
5. `icon` aus Response entfernen
|
||||
6. Felder umbenennen: `label→uiLabel`, `path→uiPath`, neu `uiComponent`
|
||||
7. `order` auf Item-Level hinzufügen
|
||||
|
||||
### Phase 2: Gateway - Access Rules Migration
|
||||
|
||||
| Datei | Änderungstyp | Aufwand | Breaking |
|
||||
|-------|--------------|---------|----------|
|
||||
| `mainTrustee.py` | Update | Mittel | Ja (DB) |
|
||||
| `mainRealEstate.py` | Update | Mittel | Ja (DB) |
|
||||
| Migration Script | Neu | Mittel | - |
|
||||
|
||||
**Konkrete Aufgaben:**
|
||||
1. `TEMPLATE_ROLES.item` auf vollqualifizierte ObjectKeys umstellen
|
||||
2. Migration: Bestehende AccessRules in DB aktualisieren
|
||||
3. Permission-Check in `_checkUiPermission` anpassen
|
||||
|
||||
### Phase 3: Gateway - User Settings
|
||||
|
||||
| Datei | Änderungstyp | Aufwand | Breaking |
|
||||
|-------|--------------|---------|----------|
|
||||
| `datamodelUam.py` | Erweitern | Gering | Nein |
|
||||
| `routeSystem.py` | Endpoint | Gering | Nein |
|
||||
|
||||
**Konkrete Aufgaben:**
|
||||
1. `UserSettings` Tabelle oder JSON-Feld in User
|
||||
2. `PUT /api/user/settings/navOrder` Endpoint
|
||||
3. Merge-Logik in Navigation-Endpoint
|
||||
|
||||
### Phase 4: Frontend
|
||||
|
||||
| Datei | Änderungstyp | Aufwand | Breaking |
|
||||
|-------|--------------|---------|----------|
|
||||
| `pageRegistry.ts` | Neu | Hoch | - |
|
||||
| `useNavigation.ts` | Refactor | Mittel | Ja |
|
||||
| `MandateNavigation.tsx` | Refactor | Hoch | Ja |
|
||||
| `mandate.ts` | Cleanup | Gering | Ja |
|
||||
|
||||
**Konkrete Aufgaben:**
|
||||
1. `PAGE_REGISTRY` erstellen mit uiComponent → Component Mapping
|
||||
2. `FEATURE_REGISTRY` mit `feature.*` Codes
|
||||
3. `useNavigation` für neuen Endpoint anpassen
|
||||
4. `MandateNavigation` für Blocks-Struktur refactoren
|
||||
5. Error-Handling für unbekannte uiComponents
|
||||
|
||||
---
|
||||
|
||||
## Kritische Punkte
|
||||
|
||||
### 1. Breaking Changes
|
||||
Die Umstellung erfordert **simultane Änderungen** in Gateway UND Frontend. Empfehlung:
|
||||
- Feature-Flag für neuen Endpoint
|
||||
- Alte Endpoints temporär behalten
|
||||
- Frontend schrittweise migrieren
|
||||
|
||||
### 2. Datenbank-Migration
|
||||
Bestehende AccessRules haben `item="dashboard"`, neu soll `item="ui.feature.trustee.dashboard"` sein.
|
||||
```sql
|
||||
-- Beispiel-Migration
|
||||
UPDATE access_rules
|
||||
SET item = 'ui.feature.trustee.' || item
|
||||
WHERE role_id IN (SELECT id FROM roles WHERE feature_code = 'trustee')
|
||||
AND context = 'UI'
|
||||
AND item IS NOT NULL;
|
||||
```
|
||||
|
||||
### 3. Performance
|
||||
Navigation mit vielen Instanzen kann langsam werden. Empfehlungen:
|
||||
- Response cachen pro User
|
||||
- Cache invalidieren bei Permission-Änderungen
|
||||
- Lazy-Loading für sehr grosse Deployments prüfen
|
||||
|
||||
### 4. Feature-spezifische View-Ableitung
|
||||
`_deriveViewPermissions` ist Trustee-spezifisch. Optionen:
|
||||
- **A)** Jedes Feature definiert seine Ableitungslogik
|
||||
- **B)** Keine Ableitung, nur explizite UI-Rules
|
||||
- **C)** Generische Mapping-Konfiguration pro Feature
|
||||
|
||||
**Empfehlung:** Option B - Explizite UI-Rules sind klarer und testbarer.
|
||||
|
||||
---
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
1. **Schritt 1:** `PAGE_REGISTRY` im Frontend erstellen (unabhängig)
|
||||
2. **Schritt 2:** Neuen `/api/navigation` Endpoint mit Feature-Flag
|
||||
3. **Schritt 3:** Frontend auf neuen Endpoint umstellen
|
||||
4. **Schritt 4:** AccessRules-Migration
|
||||
5. **Schritt 5:** User-Settings für Order
|
||||
6. **Schritt 6:** Alte Endpoints/Code entfernen
|
||||
981
implementation/RBAC-AccessRules-Refactoring.md
Normal file
981
implementation/RBAC-AccessRules-Refactoring.md
Normal file
|
|
@ -0,0 +1,981 @@
|
|||
# RBAC Access Rules Editor - Refactoring Vorschlag
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Dokument beschreibt die geplante Überarbeitung der RBAC Access Rules UI-Komponenten basierend auf drei Hauptanforderungen:
|
||||
|
||||
1. **System-Rollen bearbeitbar machen** - AccessRules für System-Rollen editierbar
|
||||
2. **Checkbox-basiertes UI** - Kompakteres Layout mit Checkboxen statt Dropdowns
|
||||
3. **Dot-Notation & Objekt-Katalog** - Verfügbare Objekte als Dropdown auswählbar
|
||||
|
||||
---
|
||||
|
||||
## 1. System-Rollen mit bearbeitbaren AccessRules
|
||||
|
||||
### Ist-Zustand
|
||||
|
||||
Aktuell in `AdminMandateRolePermissionsPage.tsx`:
|
||||
```typescript
|
||||
<AccessRulesEditor
|
||||
roleId={role.id}
|
||||
roleName={role.roleLabel}
|
||||
readOnly={role.isSystemRole} // ← System-Rollen sind komplett read-only
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
Die `readOnly`-Prop wird basierend auf `isSystemRole` gesetzt, was dazu führt, dass AccessRules für System-Rollen weder angezeigt noch bearbeitet werden können.
|
||||
|
||||
### Soll-Zustand
|
||||
|
||||
- **Rollen selbst** (roleLabel, description) bleiben geschützt für System-Rollen
|
||||
- **AccessRules** sind für alle Rollen bearbeitbar (inkl. System-Rollen)
|
||||
- Zugriffskontrolle erfolgt über RBAC selbst (`rbac.rules.manage` Permission)
|
||||
|
||||
### Implementierung
|
||||
|
||||
#### Option A: Separate Prop für Rollen-Protection
|
||||
|
||||
```typescript
|
||||
// AdminMandateRolePermissionsPage.tsx
|
||||
<AccessRulesEditor
|
||||
roleId={role.id}
|
||||
roleName={role.roleLabel}
|
||||
readOnly={false} // AccessRules immer bearbeitbar
|
||||
roleProtected={role.isSystemRole} // Nur Rolle selbst geschützt
|
||||
apiBasePath="/api/rbac"
|
||||
mandateId={selectedMandateId}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Option B: Immer bearbeitbar (empfohlen)
|
||||
|
||||
Entferne die `readOnly`-Logic basierend auf `isSystemRole`:
|
||||
|
||||
```typescript
|
||||
// AdminMandateRolePermissionsPage.tsx
|
||||
<AccessRulesEditor
|
||||
roleId={role.id}
|
||||
roleName={role.roleLabel}
|
||||
readOnly={false} // Alle AccessRules bearbeitbar
|
||||
apiBasePath="/api/rbac"
|
||||
mandateId={selectedMandateId}
|
||||
/>
|
||||
```
|
||||
|
||||
Die Zugriffskontrolle wird durch Backend-RBAC sichergestellt (nur SysAdmin kann AccessRules ändern).
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `frontend_nyla/src/pages/admin/AdminMandateRolePermissionsPage.tsx` | `readOnly={false}` setzen |
|
||||
|
||||
---
|
||||
|
||||
## 2. Checkbox-basiertes Kompakt-Layout
|
||||
|
||||
### Ist-Zustand
|
||||
|
||||
Aktuell werden AccessLevels (n/m/g/a) über Dropdown-Selects ausgewählt:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 📊 TrusteeContract [🗑] │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ VIEW READ CREATE UPDATE DELETE │
|
||||
│ [✓] [Gruppe▼] [Eigene▼] [Eigene▼] [Keine▼] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Probleme:**
|
||||
- Dropdowns benötigen Klicks zum Öffnen
|
||||
- Nicht auf einen Blick erkennbar welche Berechtigungen gesetzt sind
|
||||
- Mehrere Regeln brauchen viel vertikalen Platz
|
||||
|
||||
### Soll-Zustand
|
||||
|
||||
Kompaktes Checkbox-Grid in einer Zeile pro Objekt:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OBJEKT │ VIEW │ EIGENE │ GRUPPE │ ALLE │
|
||||
│ │ │ C R U D │ C R U D │C R U D│
|
||||
├────────────────────────────────────────────────────────────────────────────┤
|
||||
│ data.TrusteeContract │ [✓] │ [✓] [✓] [✓] [ ]│[ ] [✓] [ ] [ ]│[ ][ ][ ][ ]│
|
||||
│ data.TrusteePosition │ [✓] │ [✓] [✓] [ ] [ ]│[✓] [✓] [✓] [ ]│[ ][ ][ ][ ]│
|
||||
│ ui.feature.trustee.* │ [✓] │ - - - - │ - - - - │ - - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- Alle Berechtigungen auf einen Blick sichtbar
|
||||
- Schnelle Toggle-Aktionen mit einzelnem Klick
|
||||
- Kompakter - mehr Regeln pro Bildschirm
|
||||
- Intuitive Matrix-Darstellung (bekannt von Unix-Permissions)
|
||||
|
||||
### Neue Komponenten-Struktur
|
||||
|
||||
```
|
||||
AccessRulesEditor/
|
||||
├── AccessRulesEditor.tsx # Haupt-Container
|
||||
├── AccessRulesTable.tsx # Tabellen-basierte Darstellung (NEU)
|
||||
├── AccessRuleRow.tsx # Eine Zeile = ein AccessRule (NEU)
|
||||
├── AccessLevelCheckboxGroup.tsx # Checkbox-Gruppe für m/g/a (NEU)
|
||||
└── AccessRules.module.css # Styles
|
||||
```
|
||||
|
||||
### Neue Komponente: `AccessRulesTable.tsx`
|
||||
|
||||
```typescript
|
||||
interface AccessRulesTableProps {
|
||||
rules: AccessRule[];
|
||||
context: RuleContext;
|
||||
readOnly?: boolean;
|
||||
onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
|
||||
onDelete: (ruleId: string) => void;
|
||||
}
|
||||
|
||||
const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
||||
rules,
|
||||
context,
|
||||
readOnly,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}) => {
|
||||
const isDataContext = context === 'DATA';
|
||||
|
||||
return (
|
||||
<table className={styles.accessRulesTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.colObject}>Objekt (Dot-Notation)</th>
|
||||
<th className={styles.colView}>View</th>
|
||||
{isDataContext && (
|
||||
<>
|
||||
<th className={styles.colGroup} colSpan={4}>Eigene (m)</th>
|
||||
<th className={styles.colGroup} colSpan={4}>Gruppe (g)</th>
|
||||
<th className={styles.colGroup} colSpan={4}>Alle (a)</th>
|
||||
</>
|
||||
)}
|
||||
<th className={styles.colActions}></th>
|
||||
</tr>
|
||||
{isDataContext && (
|
||||
<tr className={styles.subHeader}>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>C</th><th>R</th><th>U</th><th>D</th>
|
||||
<th>C</th><th>R</th><th>U</th><th>D</th>
|
||||
<th>C</th><th>R</th><th>U</th><th>D</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{rules.map(rule => (
|
||||
<AccessRuleRow
|
||||
key={rule.id}
|
||||
rule={rule}
|
||||
isDataContext={isDataContext}
|
||||
readOnly={readOnly}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Neue Komponente: `AccessRuleRow.tsx`
|
||||
|
||||
```typescript
|
||||
interface AccessRuleRowProps {
|
||||
rule: AccessRule;
|
||||
isDataContext: boolean;
|
||||
readOnly?: boolean;
|
||||
onUpdate: (ruleId: string, updates: Partial<AccessRule>) => void;
|
||||
onDelete: (ruleId: string) => void;
|
||||
}
|
||||
|
||||
const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
||||
rule,
|
||||
isDataContext,
|
||||
readOnly,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}) => {
|
||||
// Hilfsfunktion: Prüft ob Level mindestens X erreicht
|
||||
const hasLevel = (level: AccessLevel | null, minLevel: 'm' | 'g' | 'a'): boolean => {
|
||||
if (!level || level === 'n') return false;
|
||||
const hierarchy = ['n', 'm', 'g', 'a'];
|
||||
return hierarchy.indexOf(level) >= hierarchy.indexOf(minLevel);
|
||||
};
|
||||
|
||||
// Hilfsfunktion: Setzt Level basierend auf Checkbox-Änderung
|
||||
const toggleLevel = (
|
||||
field: 'read' | 'create' | 'update' | 'delete',
|
||||
targetLevel: 'm' | 'g' | 'a',
|
||||
checked: boolean
|
||||
) => {
|
||||
const currentLevel = rule[field] || 'n';
|
||||
let newLevel: AccessLevel;
|
||||
|
||||
if (checked) {
|
||||
// Aktiviere mindestens dieses Level
|
||||
newLevel = targetLevel;
|
||||
} else {
|
||||
// Deaktiviere dieses Level, setze auf nächst-niedrigeres
|
||||
const hierarchy = ['n', 'm', 'g', 'a'];
|
||||
const targetIndex = hierarchy.indexOf(targetLevel);
|
||||
newLevel = hierarchy[targetIndex - 1] as AccessLevel || 'n';
|
||||
}
|
||||
|
||||
onUpdate(rule.id, { [field]: newLevel });
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className={styles.ruleRow}>
|
||||
{/* Objekt-Name in Dot-Notation */}
|
||||
<td className={styles.objectCell}>
|
||||
<code>{rule.item || '(global)'}</code>
|
||||
</td>
|
||||
|
||||
{/* View Checkbox */}
|
||||
<td className={styles.checkboxCell}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rule.view}
|
||||
onChange={(e) => onUpdate(rule.id, { view: e.target.checked })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</td>
|
||||
|
||||
{/* CRUD Checkboxen für DATA-Kontext */}
|
||||
{isDataContext && (
|
||||
<>
|
||||
{/* Eigene (m) */}
|
||||
{['create', 'read', 'update', 'delete'].map(op => (
|
||||
<td key={`m-${op}`} className={styles.checkboxCell}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasLevel(rule[op as keyof AccessRule] as AccessLevel, 'm')}
|
||||
onChange={(e) => toggleLevel(op as any, 'm', e.target.checked)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
||||
{/* Gruppe (g) */}
|
||||
{['create', 'read', 'update', 'delete'].map(op => (
|
||||
<td key={`g-${op}`} className={styles.checkboxCell}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasLevel(rule[op as keyof AccessRule] as AccessLevel, 'g')}
|
||||
onChange={(e) => toggleLevel(op as any, 'g', e.target.checked)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
||||
{/* Alle (a) */}
|
||||
{['create', 'read', 'update', 'delete'].map(op => (
|
||||
<td key={`a-${op}`} className={styles.checkboxCell}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasLevel(rule[op as keyof AccessRule] as AccessLevel, 'a')}
|
||||
onChange={(e) => toggleLevel(op as any, 'a', e.target.checked)}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Löschen-Button */}
|
||||
<td className={styles.actionsCell}>
|
||||
{!readOnly && (
|
||||
<button
|
||||
className={styles.deleteButton}
|
||||
onClick={() => onDelete(rule.id)}
|
||||
title="Regel löschen"
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### CSS für Tabellen-Layout
|
||||
|
||||
```css
|
||||
/* AccessRules.module.css - Erweiterungen */
|
||||
|
||||
.accessRulesTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.accessRulesTable th,
|
||||
.accessRulesTable td {
|
||||
padding: 0.5rem 0.375rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.accessRulesTable th {
|
||||
background: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.colObject {
|
||||
text-align: left !important;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.colView {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.colGroup {
|
||||
border-left: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.subHeader th {
|
||||
font-size: 0.6875rem;
|
||||
padding: 0.25rem;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.objectCell {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.objectCell code {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.checkboxCell {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.checkboxCell input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.actionsCell {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
padding: 0.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.deleteButton:hover {
|
||||
background: #fed7d7;
|
||||
color: #c53030;
|
||||
}
|
||||
```
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `frontend_nyla/src/components/AccessRules/AccessRulesTable.tsx` | NEU erstellen |
|
||||
| `frontend_nyla/src/components/AccessRules/AccessRuleRow.tsx` | NEU erstellen |
|
||||
| `frontend_nyla/src/components/AccessRules/AccessRulesEditor.tsx` | Integriere `AccessRulesTable` |
|
||||
| `frontend_nyla/src/components/AccessRules/AccessRules.module.css` | Tabellen-Styles hinzufügen |
|
||||
| `frontend_nyla/src/components/AccessRules/index.ts` | Exports aktualisieren |
|
||||
|
||||
---
|
||||
|
||||
## 3. Dot-Notation & RBAC-Objekt-Katalog
|
||||
|
||||
### Ist-Zustand
|
||||
|
||||
Aktuell wird das `item`-Feld als Freitext eingegeben:
|
||||
|
||||
```typescript
|
||||
// AddRuleForm.tsx (aktuell)
|
||||
<input
|
||||
type="text"
|
||||
value={item}
|
||||
onChange={(e) => setItem(e.target.value)}
|
||||
placeholder="z.B. TrusteeContract oder TrusteeContract.salary"
|
||||
/>
|
||||
```
|
||||
|
||||
**Probleme:**
|
||||
- Benutzer muss gültige Objekt-Namen kennen
|
||||
- Keine Validierung gegen registrierte Objekte
|
||||
- Inkonsistente Schreibweisen möglich
|
||||
- Keine Übersicht der verfügbaren Objekte
|
||||
|
||||
### Backend: RBAC Catalog Service
|
||||
|
||||
Der `RbacCatalogService` (`gateway/modules/security/rbacCatalog.py`) registriert bereits alle verfügbaren Objekte:
|
||||
|
||||
```python
|
||||
class RbacCatalogService:
|
||||
def __init__(self):
|
||||
self._uiObjects: Dict[str, Dict[str, Any]] = {}
|
||||
self._resourceObjects: Dict[str, Dict[str, Any]] = {}
|
||||
self._dataObjects: Dict[str, Dict[str, Any]] = {} # ← NEU
|
||||
|
||||
def getUiObjects(self, featureCode: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
# Gibt UI-Objekte zurück (z.B. "ui.feature.trustee.dashboard")
|
||||
|
||||
def getResourceObjects(self, featureCode: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
# Gibt Resource-Objekte zurück (z.B. "resource.feature.trustee.documents.create")
|
||||
```
|
||||
|
||||
**Fehlt:** DATA-Objekte werden nicht im Katalog registriert!
|
||||
|
||||
### Soll-Zustand
|
||||
|
||||
1. **Backend:** Neuer API-Endpoint zum Abrufen verfügbarer RBAC-Objekte
|
||||
2. **Backend:** DATA-Objekte (Tabellen/Entitäten) im Katalog registrieren
|
||||
3. **Frontend:** Dropdown zur Auswahl aus verfügbaren Objekten
|
||||
4. **Frontend:** Konsistente Dot-Notation für alle Objekte
|
||||
|
||||
### Backend-Implementierung
|
||||
|
||||
#### 1. DATA-Objekte im Katalog registrieren
|
||||
|
||||
Erweitere `rbacCatalog.py`:
|
||||
|
||||
```python
|
||||
# gateway/modules/security/rbacCatalog.py
|
||||
|
||||
class RbacCatalogService:
|
||||
def __init__(self):
|
||||
self._uiObjects: Dict[str, Dict[str, Any]] = {}
|
||||
self._resourceObjects: Dict[str, Dict[str, Any]] = {}
|
||||
self._dataObjects: Dict[str, Dict[str, Any]] = {} # NEU
|
||||
|
||||
def registerDataObject(
|
||||
self,
|
||||
featureCode: str,
|
||||
objectKey: str,
|
||||
label: Dict[str, str],
|
||||
meta: Optional[Dict[str, Any]] = None
|
||||
) -> bool:
|
||||
"""Register a DATA object (table/entity) for a feature."""
|
||||
try:
|
||||
self._dataObjects[objectKey] = {
|
||||
"objectKey": objectKey,
|
||||
"featureCode": featureCode,
|
||||
"label": label,
|
||||
"meta": meta or {},
|
||||
"type": "DATA"
|
||||
}
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to register DATA object {objectKey}: {e}")
|
||||
return False
|
||||
|
||||
def getDataObjects(self, featureCode: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Get all DATA objects, optionally filtered by feature."""
|
||||
if featureCode:
|
||||
return [obj for obj in self._dataObjects.values()
|
||||
if obj["featureCode"] == featureCode]
|
||||
return list(self._dataObjects.values())
|
||||
|
||||
def getAllCatalogObjects(self, featureCode: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Get all catalog objects grouped by type."""
|
||||
return {
|
||||
"DATA": self.getDataObjects(featureCode),
|
||||
"UI": self.getUiObjects(featureCode),
|
||||
"RESOURCE": self.getResourceObjects(featureCode)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. DATA-Objekte in Feature registrieren
|
||||
|
||||
Erweitere `mainTrustee.py`:
|
||||
|
||||
```python
|
||||
# gateway/modules/features/trustee/mainTrustee.py
|
||||
|
||||
# DATA Objects for RBAC catalog (Tabellen/Entitäten)
|
||||
DATA_OBJECTS = [
|
||||
{
|
||||
"objectKey": "data.feature.trustee.TrusteeContract",
|
||||
"label": {"en": "Contract", "de": "Vertrag", "fr": "Contrat"},
|
||||
"meta": {"table": "TrusteeContract", "fields": ["id", "name", "salary", ...]}
|
||||
},
|
||||
{
|
||||
"objectKey": "data.feature.trustee.TrusteePosition",
|
||||
"label": {"en": "Position", "de": "Position", "fr": "Position"},
|
||||
"meta": {"table": "TrusteePosition", "fields": ["id", "label", ...]}
|
||||
},
|
||||
{
|
||||
"objectKey": "data.feature.trustee.TrusteeDocument",
|
||||
"label": {"en": "Document", "de": "Dokument", "fr": "Document"},
|
||||
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", ...]}
|
||||
},
|
||||
]
|
||||
|
||||
def registerFeature(catalogService) -> bool:
|
||||
# ... bestehende UI/Resource Registrierung ...
|
||||
|
||||
# NEU: DATA-Objekte registrieren
|
||||
for dataObj in DATA_OBJECTS:
|
||||
catalogService.registerDataObject(
|
||||
featureCode=FEATURE_CODE,
|
||||
objectKey=dataObj["objectKey"],
|
||||
label=dataObj["label"],
|
||||
meta=dataObj.get("meta")
|
||||
)
|
||||
```
|
||||
|
||||
#### 3. Neuer API-Endpoint für Katalog-Objekte
|
||||
|
||||
Erweitere `routeAdminRbacRules.py`:
|
||||
|
||||
```python
|
||||
# gateway/modules/routes/routeAdminRbacRules.py
|
||||
|
||||
from modules.security.rbacCatalog import getCatalogService
|
||||
|
||||
@router.get("/catalog/objects", response_model=Dict[str, Any])
|
||||
@limiter.limit("60/minute")
|
||||
async def get_catalog_objects(
|
||||
request: Request,
|
||||
context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"),
|
||||
featureCode: Optional[str] = Query(None, description="Filter by feature code"),
|
||||
currentUser: User = Depends(requireSysAdmin)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get available RBAC catalog objects.
|
||||
Returns all registered DATA, UI and RESOURCE objects that can be used in AccessRules.
|
||||
|
||||
Query Parameters:
|
||||
- context: Optional filter by context type (DATA, UI, RESOURCE)
|
||||
- featureCode: Optional filter by feature (e.g., "trustee")
|
||||
|
||||
Returns:
|
||||
- Dictionary with objects grouped by context type, each with:
|
||||
- objectKey: Dot-notation identifier (e.g., "data.feature.trustee.TrusteeContract")
|
||||
- label: Multilingual label
|
||||
- featureCode: Owning feature
|
||||
- meta: Additional metadata
|
||||
|
||||
Examples:
|
||||
- GET /api/rbac/catalog/objects → all objects
|
||||
- GET /api/rbac/catalog/objects?context=DATA → only DATA objects
|
||||
- GET /api/rbac/catalog/objects?featureCode=trustee → only trustee objects
|
||||
"""
|
||||
try:
|
||||
catalog = getCatalogService()
|
||||
|
||||
if context:
|
||||
# Einzelner Context
|
||||
try:
|
||||
accessContext = AccessRuleContext(context.upper())
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid context '{context}'. Must be one of: DATA, UI, RESOURCE"
|
||||
)
|
||||
|
||||
if accessContext == AccessRuleContext.DATA:
|
||||
objects = catalog.getDataObjects(featureCode)
|
||||
elif accessContext == AccessRuleContext.UI:
|
||||
objects = catalog.getUiObjects(featureCode)
|
||||
else:
|
||||
objects = catalog.getResourceObjects(featureCode)
|
||||
|
||||
return {context.upper(): objects}
|
||||
else:
|
||||
# Alle Contexts
|
||||
return catalog.getAllCatalogObjects(featureCode)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting catalog objects: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to get catalog objects: {str(e)}"
|
||||
)
|
||||
```
|
||||
|
||||
### Frontend-Implementierung
|
||||
|
||||
#### 1. Hook für Katalog-Objekte
|
||||
|
||||
Erstelle `useCatalogObjects.ts`:
|
||||
|
||||
```typescript
|
||||
// frontend_nyla/src/hooks/useCatalogObjects.ts
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import api from '../api';
|
||||
import { RuleContext } from './useAccessRules';
|
||||
|
||||
export interface CatalogObject {
|
||||
objectKey: string;
|
||||
featureCode: string;
|
||||
label: { [lang: string]: string };
|
||||
meta?: Record<string, any>;
|
||||
type: RuleContext;
|
||||
}
|
||||
|
||||
interface CatalogObjects {
|
||||
DATA: CatalogObject[];
|
||||
UI: CatalogObject[];
|
||||
RESOURCE: CatalogObject[];
|
||||
}
|
||||
|
||||
export function useCatalogObjects() {
|
||||
const [objects, setObjects] = useState<CatalogObjects>({ DATA: [], UI: [], RESOURCE: [] });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchObjects = useCallback(async (
|
||||
context?: RuleContext,
|
||||
featureCode?: string
|
||||
): Promise<CatalogObjects> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (context) params.append('context', context);
|
||||
if (featureCode) params.append('featureCode', featureCode);
|
||||
|
||||
const url = `/api/rbac/catalog/objects${params.toString() ? `?${params}` : ''}`;
|
||||
const response = await api.get(url);
|
||||
|
||||
const data = response.data as CatalogObjects;
|
||||
setObjects(data);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
const errorMsg = err.response?.data?.detail || err.message || 'Fehler beim Laden';
|
||||
setError(errorMsg);
|
||||
return { DATA: [], UI: [], RESOURCE: [] };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getObjectsByContext = useCallback((context: RuleContext): CatalogObject[] => {
|
||||
return objects[context] || [];
|
||||
}, [objects]);
|
||||
|
||||
return {
|
||||
objects,
|
||||
loading,
|
||||
error,
|
||||
fetchObjects,
|
||||
getObjectsByContext,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Objekt-Auswahl Dropdown
|
||||
|
||||
Aktualisiere `AddRuleForm`:
|
||||
|
||||
```typescript
|
||||
// AccessRulesEditor.tsx - AddRuleForm Komponente
|
||||
|
||||
interface AddRuleFormProps {
|
||||
context: RuleContext;
|
||||
availableObjects: CatalogObject[]; // NEU
|
||||
onAdd: (rule: AccessRuleCreate) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const AddRuleForm: React.FC<AddRuleFormProps> = ({
|
||||
context,
|
||||
availableObjects, // NEU
|
||||
onAdd,
|
||||
onCancel
|
||||
}) => {
|
||||
const [item, setItem] = useState('');
|
||||
const [useCustom, setUseCustom] = useState(false); // NEU: Toggle für Freitext
|
||||
// ... rest state
|
||||
|
||||
// Gruppiere Objekte nach Feature
|
||||
const groupedObjects = useMemo(() => {
|
||||
const grouped: Record<string, CatalogObject[]> = {};
|
||||
availableObjects.forEach(obj => {
|
||||
if (!grouped[obj.featureCode]) {
|
||||
grouped[obj.featureCode] = [];
|
||||
}
|
||||
grouped[obj.featureCode].push(obj);
|
||||
});
|
||||
return grouped;
|
||||
}, [availableObjects]);
|
||||
|
||||
// Aktuelle Sprache für Labels
|
||||
const lang = useLanguage(); // oder 'de' als Default
|
||||
|
||||
return (
|
||||
<form className={styles.addRuleForm} onSubmit={handleSubmit}>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>
|
||||
Objekt auswählen
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggleCustom}
|
||||
onClick={() => setUseCustom(!useCustom)}
|
||||
>
|
||||
{useCustom ? '← Aus Katalog wählen' : 'Freie Eingabe →'}
|
||||
</button>
|
||||
</label>
|
||||
|
||||
{useCustom ? (
|
||||
// Freitext-Eingabe (wie bisher)
|
||||
<input
|
||||
type="text"
|
||||
value={item}
|
||||
onChange={(e) => setItem(e.target.value)}
|
||||
placeholder={getPlaceholder()}
|
||||
className={styles.formInput}
|
||||
/>
|
||||
) : (
|
||||
// Dropdown mit verfügbaren Objekten
|
||||
<select
|
||||
value={item}
|
||||
onChange={(e) => setItem(e.target.value)}
|
||||
className={styles.formSelect}
|
||||
>
|
||||
<option value="">-- Global (alle Objekte) --</option>
|
||||
{Object.entries(groupedObjects).map(([feature, objs]) => (
|
||||
<optgroup key={feature} label={feature.toUpperCase()}>
|
||||
{objs.map(obj => (
|
||||
<option key={obj.objectKey} value={obj.objectKey}>
|
||||
{obj.objectKey} - {obj.label[lang] || obj.label.en}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
|
||||
<span className={styles.formHint}>
|
||||
Leer lassen für globale Regel. Längster Match gewinnt.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ... rest of form */}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Katalog-Objekte im Editor laden
|
||||
|
||||
```typescript
|
||||
// AccessRulesEditor.tsx
|
||||
|
||||
export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
||||
roleId,
|
||||
roleName,
|
||||
readOnly = false,
|
||||
apiBasePath = '/api/rbac',
|
||||
mandateId,
|
||||
}) => {
|
||||
// ... bestehende hooks
|
||||
|
||||
// NEU: Katalog-Objekte laden
|
||||
const { objects: catalogObjects, fetchObjects } = useCatalogObjects();
|
||||
|
||||
useEffect(() => {
|
||||
fetchObjects(); // Alle Objekte laden beim Mount
|
||||
}, [fetchObjects]);
|
||||
|
||||
// Objekte für aktuellen Tab filtern
|
||||
const currentContextObjects = useMemo(() => {
|
||||
return catalogObjects[activeTab] || [];
|
||||
}, [catalogObjects, activeTab]);
|
||||
|
||||
return (
|
||||
<div className={styles.accessRulesEditor}>
|
||||
{/* ... header, tabs */}
|
||||
|
||||
<div className={styles.tabContent}>
|
||||
{activeTab !== 'JSON' && (
|
||||
<RulesSection
|
||||
context={activeTab}
|
||||
rules={groupedRules[activeTab]}
|
||||
availableObjects={currentContextObjects} // NEU
|
||||
readOnly={readOnly}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
)}
|
||||
{/* ... JSON tab */}
|
||||
</div>
|
||||
|
||||
{/* ... action bar */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Dot-Notation Schema
|
||||
|
||||
Konsistentes Namensschema für alle RBAC-Objekte:
|
||||
|
||||
```
|
||||
<type>.<scope>.<feature>.<entity>[.<field>]
|
||||
|
||||
Beispiele:
|
||||
├── DATA
|
||||
│ ├── data.feature.trustee.TrusteeContract
|
||||
│ ├── data.feature.trustee.TrusteeContract.salary (Feld-Level)
|
||||
│ ├── data.feature.trustee.TrusteePosition
|
||||
│ ├── data.system.UserInDB (System-Entität)
|
||||
│ └── data.system.Mandate
|
||||
│
|
||||
├── UI
|
||||
│ ├── ui.feature.trustee.dashboard
|
||||
│ ├── ui.feature.trustee.positions
|
||||
│ ├── ui.admin.users (Admin-Bereich)
|
||||
│ ├── ui.playground.voice.settings (Playground)
|
||||
│ └── ui.nav.trustee (Navigation)
|
||||
│
|
||||
└── RESOURCE
|
||||
├── resource.feature.trustee.documents.create
|
||||
├── resource.feature.trustee.instance-roles.manage
|
||||
├── resource.ai.model.anthropic (AI-Resources)
|
||||
└── resource.connector.sharepoint (Connectors)
|
||||
```
|
||||
|
||||
### Betroffene Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| **Backend** ||
|
||||
| `gateway/modules/security/rbacCatalog.py` | `registerDataObject()`, `getDataObjects()`, `getAllCatalogObjects()` |
|
||||
| `gateway/modules/routes/routeAdminRbacRules.py` | Neuer Endpoint `GET /api/rbac/catalog/objects` |
|
||||
| `gateway/modules/features/trustee/mainTrustee.py` | `DATA_OBJECTS` Liste, Registrierung erweitern |
|
||||
| `gateway/modules/features/*/main*.py` | DATA_OBJECTS in allen Features |
|
||||
| **Frontend** ||
|
||||
| `frontend_nyla/src/hooks/useCatalogObjects.ts` | NEU erstellen |
|
||||
| `frontend_nyla/src/hooks/index.ts` | Export hinzufügen |
|
||||
| `frontend_nyla/src/components/AccessRules/AccessRulesEditor.tsx` | Katalog-Objekte integrieren |
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung der Änderungen
|
||||
|
||||
### Backend
|
||||
|
||||
| Priorität | Datei | Beschreibung |
|
||||
|-----------|-------|--------------|
|
||||
| 1 | `rbacCatalog.py` | DATA-Objekte Registrierung |
|
||||
| 1 | `routeAdminRbacRules.py` | `/catalog/objects` Endpoint |
|
||||
| 2 | `mainTrustee.py` | DATA_OBJECTS definieren |
|
||||
| 2 | Alle Feature `main*.py` | DATA_OBJECTS in allen Features |
|
||||
|
||||
### Frontend
|
||||
|
||||
| Priorität | Datei | Beschreibung |
|
||||
|-----------|-------|--------------|
|
||||
| 1 | `AdminMandateRolePermissionsPage.tsx` | `readOnly={false}` für System-Rollen |
|
||||
| 1 | `AccessRulesTable.tsx` | Neue Tabellen-Komponente |
|
||||
| 1 | `AccessRuleRow.tsx` | Zeilen-Komponente mit Checkboxen |
|
||||
| 2 | `useCatalogObjects.ts` | Hook für Katalog-Objekte |
|
||||
| 2 | `AccessRulesEditor.tsx` | Integration Katalog + Tabelle |
|
||||
| 2 | `AccessRules.module.css` | Styles für Tabelle |
|
||||
|
||||
### Migrations-Strategie
|
||||
|
||||
1. **Phase 1:** Backend-Erweiterungen (Katalog + Endpoint)
|
||||
2. **Phase 2:** Frontend Checkbox-UI (ersetzt Dropdown)
|
||||
3. **Phase 3:** Katalog-Integration im Frontend
|
||||
4. **Phase 4:** System-Rollen bearbeitbar machen
|
||||
|
||||
---
|
||||
|
||||
## Mockups
|
||||
|
||||
### Kompakte Tabellen-Ansicht (Phase 2)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 📊 DATEN-REGELN [+ Neue Regel] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │ │ EIGENE │ GRUPPE │ ALLE │
|
||||
│ OBJEKT │VIEW │ C R U D │ C R U D │C R U D│
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ data.feature.trustee.* │ ✓ │ ✓ ✓ ✓ □ │ □ ✓ □ □ │□ □ □ □│ 🗑
|
||||
│ data.feature.trustee.docs │ ✓ │ ✓ ✓ ✓ ✓ │ ✓ ✓ ✓ ✓ │✓ ✓ ✓ ✓│ 🗑
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 🖥 UI-REGELN [+ Neue Regel] │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ OBJEKT │ VIEW │ │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ui.feature.trustee.dashboard │ ✓ │ 🗑 │
|
||||
│ ui.feature.trustee.positions │ ✓ │ 🗑 │
|
||||
│ ui.feature.trustee.documents │ ✓ │ 🗑 │
|
||||
│ ui.admin.mandate-roles │ □ │ 🗑 │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Objekt-Auswahl Dropdown (Phase 3)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Neue Regel hinzufügen │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Objekt auswählen: [Freie Eingabe →] │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ -- Global (alle Objekte) -- ▼│ │
|
||||
│ ├─────────────────────────────────────────────────────────────┤ │
|
||||
│ │ ▸ TRUSTEE │ │
|
||||
│ │ data.feature.trustee.TrusteeContract - Vertrag │ │
|
||||
│ │ data.feature.trustee.TrusteePosition - Position │ │
|
||||
│ │ data.feature.trustee.TrusteeDocument - Dokument │ │
|
||||
│ │ ▸ SYSTEM │ │
|
||||
│ │ data.system.UserInDB - Benutzer │ │
|
||||
│ │ data.system.Mandate - Mandant │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [✓] Sichtbar (View) │
|
||||
│ │
|
||||
│ [Abbrechen] [Hinzufügen] │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen
|
||||
|
||||
1. **Feature-Filter:** Soll der Katalog nach aktiven Features des Mandanten gefiltert werden? Ja, der Katalog soll nach aktiven Features des Mandanten gefiltert werden.
|
||||
2. **Wildcard-Patterns:** Unterstützung für `data.feature.trustee.*` (alle Objekte eines Features)? Ja, die Unterstützung für `data.feature.trustee.*` (alle Objekte eines Features) soll implementiert werden.
|
||||
3. **Feld-Level Permissions:** Sollen einzelne Felder (z.B. `TrusteeContract.salary`) unterstützt werden? Ja, die Unterstützung für einzelne Felder (z.B. `TrusteeContract.salary`) soll implementiert werden.
|
||||
4. **Vererbung:** Sollen Berechtigungen von übergeordneten Objekten vererbt werden? Ja, die Vererbung von Berechtigungen von übergeordneten Objekten soll implementiert werden.
|
||||
|
||||
---
|
||||
|
||||
*Erstellt: 2026-01-24*
|
||||
*Status: Entwurf zur Diskussion*
|
||||
Loading…
Reference in a new issue