From d8d692200e822013a23b60c81468fe8ddce137c5 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 14 Jan 2026 22:38:46 +0100
Subject: [PATCH] tenant concepts
---
.../implementation_concept.md | 827 ++++++++++++++++
.../mandate_concept.md | 609 ++++++++++++
.../ui_concept_nyla.md | 880 ++++++++++++++++++
3 files changed, 2316 insertions(+)
create mode 100644 implementation/Saas Multi Tenant Mandate/implementation_concept.md
create mode 100644 implementation/Saas Multi Tenant Mandate/mandate_concept.md
create mode 100644 implementation/Saas Multi Tenant Mandate/ui_concept_nyla.md
diff --git a/implementation/Saas Multi Tenant Mandate/implementation_concept.md b/implementation/Saas Multi Tenant Mandate/implementation_concept.md
new file mode 100644
index 0000000..2e40ab0
--- /dev/null
+++ b/implementation/Saas Multi Tenant Mandate/implementation_concept.md
@@ -0,0 +1,827 @@
+# Multi-Tenant SaaS Plattform - Implementierungskonzept
+
+## Technische Umsetzung für PowerOn Gateway
+
+**Version:** 1.0
+**Datum:** 14. Januar 2026
+**Status:** Entwurf
+**Basis:** [mandate_concept.md](./mandate_concept.md)
+
+---
+
+## 1. Übersicht
+
+### 1.1 Grundprinzip
+
+**Kernänderung:** User gehören grundsätzlich **keinem** Mandanten an. Die Zugehörigkeit wird über eine separate Membership-Tabelle (`UserMandate`) geregelt.
+
+```
+AKTUELL (IST):
+User ──── mandateId ──→ Mandate (1:1)
+
+NEU (SOLL):
+User ◄──── UserMandate ────► Mandate (m:n)
+ └─ roleLabels (mandantsspezifisch)
+```
+
+### 1.2 Konsequenzen
+
+| Aspekt | Aktuell | Neu |
+|--------|---------|-----|
+| User.mandateId | Pflichtfeld | Optional/Entfällt |
+| Rollen | Global pro User | Pro Membership |
+| Auth-Token | Enthält mandateId | Enthält nur userId |
+| API-Calls | Implizit aus Token | Explizit pro Request |
+| RBAC GROUP | User.mandateId | Request-Parameter |
+
+---
+
+## 2. Feature-Analyse (Code-Review)
+
+### 2.1 Identifizierte Business-Features
+
+Basierend auf Code-Analyse (`gateway/modules/`) wurden folgende Features identifiziert, die in das neue Multi-Tenant-Modell überführt werden müssen:
+
+| Feature-Code | Name | Datenmodelle | Routes | Feature-Code |
+|--------------|------|--------------|--------|--------------|
+| `trustee` | **Treuhand** | `TrusteeOrganisation`, `TrusteeRole`, `TrusteeAccess`, `TrusteeContract`, `TrusteeDocument`, `TrusteePosition` | `routeDataTrustee.py` | `interfaceDbTrusteeObjects.py` |
+| `chatbot` | **Chatbot** | `ChatWorkflow` (mode=Chatbot), `ChatMessage`, `ChatDocument` | `routeChatbot.py` | `features/chatbot/` |
+| `workflow-dynamic` | **Dynamic Workflow** | `ChatWorkflow` (mode=Dynamic), `ChatMessage`, `ChatDocument`, `ChatLog`, `ChatStat` | `routeChatPlayground.py`, `routeWorkflows.py` | `features/workflow/` + `WorkflowManager` |
+| `workflow-automation` | **Automation** | `AutomationDefinition`, `ChatWorkflow` (mode=Automation) | `routeDataAutomation.py`, `routeAdminAutomationEvents.py` | `features/workflow/` + `WorkflowManager` |
+| `voice-center` | **Voice Center** | `VoiceSettings` | `routeVoiceGoogle.py` | `interfaceVoiceObjects.py` |
+| `realestate` | **Immobilien** | `Projekt`, `Parzelle`, `Land`, `Kanton`, `Gemeinde`, `Dokument` | `routeRealEstate.py` | `interfaceDbRealEstateObjects.py` |
+
+### 2.2 Workflow-Modes und Feature-Zuordnung
+
+Der `WorkflowModeEnum` definiert verschiedene Modi:
+
+```python
+# datamodelChat.py - Zeile 277-281
+class WorkflowModeEnum(str, Enum):
+ WORKFLOW_DYNAMIC = "Dynamic" # → Feature: workflow-dynamic (WorkflowManager)
+ WORKFLOW_AUTOMATION = "Automation" # → Feature: workflow-automation (WorkflowManager + Templates)
+ WORKFLOW_CHATBOT = "Chatbot" # → Feature: chatbot (eigener Code in features/chatbot/)
+ WORKFLOW_REACT = "React" # Legacy - kann entfallen
+```
+
+**Code-Struktur:**
+
+| Feature | Code-Basis | Beschreibung |
+|---------|-----------|--------------|
+| `chatbot` | `features/chatbot/mainChatbot.py` → `chatProcess()` | Einfacher Chat mit direkten AI-Calls |
+| `workflow-dynamic` | `features/workflow/mainWorkflow.py` → `chatStart()` | Interaktiver KI-Workflow mit Task-Planning |
+| `workflow-automation` | `features/workflow/mainWorkflow.py` → `executeAutomation()` | Zeitgesteuerte Workflows mit Templates |
+
+**Hinweis:** `routeChatPlayground.py` ist **keine eigenes Feature**, sondern eine Test-Route, die sowohl `workflow-dynamic` als auch `workflow-automation` aufrufen kann (via Query-Parameter `workflowMode`).
+
+**Begründung für Feature-Trennung:**
+- Unterschiedliche Berechtigungen pro Mode möglich
+- Unterschiedliche Instanzen pro Mandant (z.B. Mandant A hat Automation, Mandant B nicht)
+- Unterschiedliche Pricing-Modelle
+
+### 2.3 System-Features (ohne Mandant)
+
+Diese Features sind **global** und erfordern **keinen Mandanten**:
+
+| Feature | Beschreibung | Routes |
+|---------|--------------|--------|
+| `user-profile` | Eigenes Profil bearbeiten | `routeDataUsers.py` |
+| `user-settings` | Persönliche Einstellungen | `routeOptions.py` |
+| `file-management` | Dateiverwaltung (mandantenübergreifend) | `routeDataFiles.py` |
+| `connections` | Verbindungen (OAuth, APIs) | `routeDataConnections.py` |
+
+### 2.4 Admin-Features (nur sysadmin)
+
+Diese Features sind nur für System-Administratoren:
+
+| Feature | Beschreibung | Routes |
+|---------|--------------|--------|
+| `admin-mandates` | Mandanten verwalten | `routeDataMandates.py` |
+| `admin-users` | Alle User verwalten | `routeDataUsers.py` |
+| `admin-rbac` | RBAC-Regeln verwalten | `routeRbac.py`, `routeAdminRbacRoles.py` |
+| `admin-security` | Security-Einstellungen | `routeSecurityAdmin.py` |
+
+### 2.5 Mandant-spezifische Felder in Datamodels
+
+Alle Business-Feature-Models haben ein `mandateId`-Feld:
+
+```python
+# Beispiele aus dem Code:
+
+# datamodelChat.py - ChatWorkflow
+mandateId: str = Field(description="ID of the mandate this workflow belongs to")
+
+# datamodelVoice.py - VoiceSettings
+mandateId: str = Field(description="ID of the mandate these settings belong to")
+
+# datamodelTrustee.py - TrusteeOrganisation
+mandateId: Optional[str] = Field(default=None, description="Mandate ID")
+
+# datamodelRealEstate.py - Projekt, Parzelle, etc.
+mandateId: str = Field(description="ID of the mandate")
+```
+
+### 2.6 Zu ergänzen: Feature-Definition Model
+
+Neues Model für Feature-Registrierung:
+
+```python
+# modules/datamodels/datamodelFeatures.py (NEU)
+
+class FeatureDefinition(BaseModel):
+ """Definition eines Features im System."""
+ code: str = Field(description="Eindeutiger Feature-Code (z.B. 'trustee')")
+ label: Dict[str, str] = Field(description="I18n-Labels {'en': '...', 'de': '...'}")
+ icon: str = Field(description="Material Icon Name")
+ category: str = Field(description="Kategorie: 'business', 'system', 'admin'")
+ tables: List[str] = Field(description="Zugehörige DB-Tabellen")
+ routes: List[str] = Field(description="API-Route-Prefixe")
+ defaultRoles: List[str] = Field(description="Standard-Rollen für dieses Feature")
+ enabled: bool = Field(default=True)
+
+
+class FeatureInstance(BaseModel):
+ """Instanz eines Features für einen Mandanten."""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
+ featureCode: str = Field(description="Referenz auf FeatureDefinition.code")
+ mandateId: str = Field(description="Referenz auf Mandate.id")
+ instanceLabel: str = Field(description="Instanz-Name (z.B. 'Buchhaltung 2025')")
+ enabled: bool = Field(default=True)
+ settings: Dict[str, Any] = Field(default_factory=dict, description="Feature-spezifische Einstellungen")
+
+
+class FeatureAccess(BaseModel):
+ """Zugriff eines Users auf eine Feature-Instanz."""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
+ userId: str = Field(description="Referenz auf User.id")
+ featureInstanceId: str = Field(description="Referenz auf FeatureInstance.id")
+ roleLabel: str = Field(description="Rolle in dieser Feature-Instanz")
+ enabled: bool = Field(default=True)
+```
+
+---
+
+## 3. Datenmodell-Änderungen
+
+### 3.1 Neues Model: `UserMandate`
+
+```python
+# modules/datamodels/datamodelUam.py
+
+class UserMandate(BaseModel):
+ """Membership: Verknüpft User mit Mandanten und deren Rollen."""
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
+ userId: str = Field(description="Referenz auf User.id")
+ mandateId: str = Field(description="Referenz auf Mandate.id")
+ roleLabels: List[str] = Field(
+ default_factory=lambda: ["user"],
+ description="Rollen des Users in diesem Mandanten"
+ )
+ enabled: bool = Field(default=True)
+ invitedBy: Optional[str] = Field(None, description="User-ID des Einladenden")
+ invitedAt: Optional[float] = Field(None, description="Zeitpunkt der Einladung")
+ acceptedAt: Optional[float] = Field(None, description="Zeitpunkt der Annahme")
+ # System-Felder: _createdAt, _modifiedAt, _createdBy, _modifiedBy
+```
+
+### 3.2 Änderung: `User` Model
+
+```python
+# modules/datamodels/datamodelUam.py
+
+class User(BaseModel):
+ id: str = Field(...)
+ username: str = Field(...)
+ email: Optional[EmailStr] = Field(None)
+ fullName: Optional[str] = Field(None)
+ language: str = Field(default="en")
+ enabled: bool = Field(default=True)
+ # ENTFERNT: mandateId - User gehören keinem Mandanten
+ # ENTFERNT: roleLabels - Rollen sind mandantsspezifisch
+ authenticationAuthority: AuthAuthority = Field(default=AuthAuthority.LOCAL)
+
+ # NEU: Transiente Felder (nicht in DB, werden zur Laufzeit befüllt)
+ currentMandateId: Optional[str] = Field(None, exclude=True)
+ currentRoleLabels: List[str] = Field(default_factory=list, exclude=True)
+```
+
+### 3.3 Änderung: `AccessRule` Model
+
+```python
+# modules/datamodels/datamodelRbac.py
+
+class AccessRule(BaseModel):
+ id: str = Field(...)
+ roleLabel: str = Field(...)
+ context: AccessRuleContext = Field(...)
+ item: Optional[str] = Field(None)
+ view: bool = Field(False)
+ read: Optional[AccessLevel] = Field(None)
+ create: Optional[AccessLevel] = Field(None)
+ update: Optional[AccessLevel] = Field(None)
+ delete: Optional[AccessLevel] = Field(None)
+ # NEU: Mandant-spezifische Regeln
+ mandateId: Optional[str] = Field(
+ None,
+ description="Wenn gesetzt, gilt Regel nur für diesen Mandanten. None = Global."
+ )
+```
+
+---
+
+## 4. Authentifizierung & Kontext
+
+### 4.1 JWT Token (vereinfacht)
+
+**Aktuell:**
+```json
+{
+ "sub": "admin",
+ "userId": "uuid-123",
+ "mandateId": "uuid-456",
+ "exp": 1234567890
+}
+```
+
+**Neu:**
+```json
+{
+ "sub": "admin",
+ "userId": "uuid-123",
+ "exp": 1234567890
+}
+```
+
+Der Token enthält **kein mandateId** mehr, da der User keinem Mandanten angehört.
+
+### 4.2 Mandanten-Kontext per Request
+
+Der Mandanten-Kontext wird **pro API-Call** bestimmt:
+
+**Option A: Query-Parameter**
+```
+GET /api/trustee/organisations?mandateId=uuid-456
+```
+
+**Option B: Header**
+```
+X-Mandate-Id: uuid-456
+```
+
+**Option C: Implizit aus Daten**
+```
+POST /api/trustee/contracts
+Body: { "organisationId": "org-123" }
+→ Backend ermittelt mandateId aus Organisation
+```
+
+### 4.3 Authentifizierung Flow
+
+```
+1. Login
+ └─ User authentifiziert sich
+ └─ Token enthält nur userId
+ └─ Response enthält Liste der Memberships
+
+2. API-Call
+ └─ Token wird validiert (userId)
+ └─ mandateId wird aus Request extrahiert
+ └─ Membership wird geprüft (userId + mandateId)
+ └─ Rollen werden aus UserMandate geladen
+ └─ RBAC wird mit diesen Rollen ausgeführt
+```
+
+---
+
+## 5. RBAC-Anpassungen
+
+### 5.1 Regel-Auflösung (Mandant-aware)
+
+```python
+# modules/security/rbac.py
+
+def _getRulesForRole(self, roleLabel: str, context: AccessRuleContext, mandateId: Optional[str] = None) -> List[AccessRule]:
+ """
+ Lädt Regeln mit Mandant-Priorisierung:
+ 1. Mandant-spezifische Regeln (mandateId = X)
+ 2. Globale Regeln (mandateId = None)
+
+ Spezifische überschreiben globale.
+ """
+ allRules = self.dbApp.getRecordset(
+ AccessRule,
+ recordFilter={"roleLabel": roleLabel, "context": context.value}
+ )
+
+ # Sortiere: mandateId-spezifisch vor global
+ mandateRules = [r for r in allRules if r.get("mandateId") == mandateId]
+ globalRules = [r for r in allRules if r.get("mandateId") is None]
+
+ # Spezifische haben Vorrang
+ rulesByItem = {}
+ for rule in globalRules:
+ rulesByItem[rule.get("item")] = rule
+ for rule in mandateRules:
+ rulesByItem[rule.get("item")] = rule # Überschreibt global
+
+ return list(rulesByItem.values())
+```
+
+### 5.2 GROUP-Filter anpassen
+
+```python
+# modules/interfaces/interfaceRbac.py
+
+def buildRbacWhereClause(
+ permissions: UserPermissions,
+ currentUser: User,
+ table: str,
+ connector,
+ mandateId: str # NEU: Expliziter Parameter
+) -> Optional[Dict[str, Any]]:
+ """
+ GROUP filtert jetzt nach übergebenem mandateId,
+ nicht mehr nach currentUser.mandateId.
+ """
+ if permissions.read == AccessLevel.GROUP:
+ if not mandateId:
+ return {"condition": "1 = 0", "values": []}
+
+ return {
+ "condition": '"mandateId" = %s',
+ "values": [mandateId]
+ }
+ # ... rest bleibt gleich
+```
+
+---
+
+## 6. Interface-Anpassungen
+
+### 6.1 Kontext ohne Pflicht-Mandat
+
+```python
+# modules/interfaces/interfaceDbAppObjects.py
+
+def setUserContext(self, currentUser: User, mandateId: Optional[str] = None):
+ """
+ Setzt User-Kontext. mandateId ist optional.
+ Wenn mandateId gesetzt, wird Membership geprüft.
+ """
+ self.currentUser = currentUser
+ self.userId = currentUser.id
+ self.currentMandateId = mandateId
+
+ if mandateId:
+ # Membership prüfen und Rollen laden
+ membership = self.getUserMandate(currentUser.id, mandateId)
+ if not membership:
+ raise PermissionError(f"User {currentUser.id} ist nicht Mitglied in Mandant {mandateId}")
+
+ # Rollen aus Membership für RBAC verwenden
+ currentUser.currentRoleLabels = membership.roleLabels
+ else:
+ # Ohne Mandat: System-Rollen (sysadmin) oder leer
+ currentUser.currentRoleLabels = ["sysadmin"] if self._isSysAdmin(currentUser) else []
+```
+
+### 6.2 Neue Membership-Methoden
+
+```python
+# modules/interfaces/interfaceDbAppObjects.py
+
+def getUserMandates(self, userId: str) -> List[UserMandate]:
+ """Alle Mandanten-Mitgliedschaften eines Users."""
+ records = self.db.getRecordset(UserMandate, recordFilter={"userId": userId})
+ return [UserMandate(**r) for r in records]
+
+def getUserMandate(self, userId: str, mandateId: str) -> Optional[UserMandate]:
+ """Spezifische Mitgliedschaft prüfen."""
+ records = self.db.getRecordset(
+ UserMandate,
+ recordFilter={"userId": userId, "mandateId": mandateId}
+ )
+ return UserMandate(**records[0]) if records else None
+
+def getMandateMembers(self, mandateId: str) -> List[UserMandate]:
+ """Alle Mitglieder eines Mandanten."""
+ records = self.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
+ return [UserMandate(**r) for r in records]
+
+def addUserToMandate(self, userId: str, mandateId: str, roleLabels: List[str], invitedBy: str) -> UserMandate:
+ """Fügt User zu Mandant hinzu."""
+ membership = UserMandate(
+ userId=userId,
+ mandateId=mandateId,
+ roleLabels=roleLabels,
+ invitedBy=invitedBy,
+ invitedAt=getUtcTimestamp()
+ )
+ result = self.db.recordCreate(UserMandate, membership)
+ return UserMandate(**result)
+
+def removeUserFromMandate(self, userId: str, mandateId: str) -> bool:
+ """Entfernt User aus Mandant."""
+ memberships = self.db.getRecordset(
+ UserMandate,
+ recordFilter={"userId": userId, "mandateId": mandateId}
+ )
+ if memberships:
+ return self.db.recordDelete(UserMandate, memberships[0]["id"])
+ return False
+
+def updateMembershipRoles(self, membershipId: str, roleLabels: List[str]) -> UserMandate:
+ """Aktualisiert Rollen einer Mitgliedschaft."""
+ result = self.db.recordModify(UserMandate, membershipId, {"roleLabels": roleLabels})
+ return UserMandate(**result)
+```
+
+---
+
+## 6. API-Routen
+
+### 6.1 Neue Membership-Routen
+
+```python
+# modules/routes/routeMandateMemberships.py
+
+router = APIRouter(prefix="/api/mandates", tags=["Mandate Memberships"])
+
+@router.get("/my", response_model=List[MandateMembershipResponse])
+async def getMyMandates(currentUser: User = Depends(getCurrentUser)):
+ """Alle Mandanten des eingeloggten Users."""
+ interface = getInterface(currentUser)
+ memberships = interface.getUserMandates(currentUser.id)
+
+ result = []
+ for m in memberships:
+ mandate = interface.getMandate(m.mandateId)
+ result.append({
+ "membershipId": m.id,
+ "mandateId": m.mandateId,
+ "mandateName": mandate.name if mandate else "Unknown",
+ "roleLabels": m.roleLabels,
+ "enabled": m.enabled
+ })
+ return result
+
+@router.get("/{mandateId}/members", response_model=List[MemberResponse])
+async def getMandateMembers(
+ mandateId: str,
+ currentUser: User = Depends(getCurrentUser)
+):
+ """Alle Mitglieder eines Mandanten (nur für Admins)."""
+ interface = getInterface(currentUser, mandateId)
+ # Prüft automatisch Membership und admin-Rolle
+ return interface.getMandateMembers(mandateId)
+
+@router.post("/{mandateId}/members")
+async def addMember(
+ mandateId: str,
+ data: AddMemberRequest,
+ currentUser: User = Depends(getCurrentUser)
+):
+ """Fügt User zu Mandant hinzu (nur sysadmin oder mandant-admin)."""
+ interface = getInterface(currentUser, mandateId)
+ return interface.addUserToMandate(
+ userId=data.userId,
+ mandateId=mandateId,
+ roleLabels=data.roleLabels,
+ invitedBy=currentUser.id
+ )
+
+@router.delete("/{mandateId}/members/{userId}")
+async def removeMember(
+ mandateId: str,
+ userId: str,
+ currentUser: User = Depends(getCurrentUser)
+):
+ """Entfernt User aus Mandant."""
+ interface = getInterface(currentUser, mandateId)
+ return interface.removeUserFromMandate(userId, mandateId)
+
+@router.put("/{mandateId}/members/{userId}/roles")
+async def updateMemberRoles(
+ mandateId: str,
+ userId: str,
+ data: UpdateRolesRequest,
+ currentUser: User = Depends(getCurrentUser)
+):
+ """Aktualisiert Rollen eines Mitglieds."""
+ interface = getInterface(currentUser, mandateId)
+ membership = interface.getUserMandate(userId, mandateId)
+ return interface.updateMembershipRoles(membership.id, data.roleLabels)
+```
+
+### 7.2 Anpassung bestehender Routen
+
+```python
+# modules/routes/routeDataUsers.py
+
+@router.get("/")
+async def getUsers(
+ mandateId: Optional[str] = Query(None), # Explizit, nicht aus Token
+ currentUser: User = Depends(getCurrentUser)
+):
+ """
+ Users abrufen.
+ - Ohne mandateId: Nur sysadmin sieht alle
+ - Mit mandateId: Mitglieder des Mandanten
+ """
+ if mandateId:
+ interface = getInterface(currentUser, mandateId)
+ memberships = interface.getMandateMembers(mandateId)
+ userIds = [m.userId for m in memberships]
+ return interface.getUsersByIds(userIds)
+ else:
+ # Nur sysadmin
+ if not _isSysAdmin(currentUser):
+ raise HTTPException(403, "mandateId required for non-sysadmin")
+ interface = getInterface(currentUser)
+ return interface.getAllUsers()
+```
+
+---
+
+## 8. RBAC Import/Export
+
+### 8.1 Export-Format (JSON)
+
+```json
+{
+ "version": "1.0",
+ "exportedAt": "2026-01-14T10:00:00Z",
+ "exportedBy": "admin",
+ "scope": {
+ "mandateId": "uuid-456",
+ "description": "Althaus Consulting - Custom Policies"
+ },
+ "roles": [
+ {
+ "roleLabel": "trustee-admin",
+ "description": {"en": "Trustee Administrator", "de": "Treuhand-Administrator"},
+ "isSystemRole": false
+ }
+ ],
+ "rules": [
+ {
+ "roleLabel": "trustee-admin",
+ "context": "DATA",
+ "item": "TrusteeContract",
+ "view": true,
+ "read": "g",
+ "create": "g",
+ "update": "g",
+ "delete": "g",
+ "mandateId": "uuid-456"
+ }
+ ]
+}
+```
+
+### 8.2 Export-Endpoint
+
+```python
+# modules/routes/routeRbac.py
+
+@router.get("/export")
+async def exportRbac(
+ mandateId: Optional[str] = Query(None),
+ currentUser: User = Depends(getCurrentUser)
+):
+ """
+ Exportiert RBAC-Konfiguration.
+ - mandateId=None: Globale Regeln
+ - mandateId=X: Regeln für Mandant X
+ """
+ interface = getInterface(currentUser, mandateId)
+
+ roles = interface.getAllRoles()
+ rules = interface.getAccessRules(mandateId=mandateId)
+
+ return {
+ "version": "1.0",
+ "exportedAt": getUtcTimestamp(),
+ "exportedBy": currentUser.username,
+ "scope": {
+ "mandateId": mandateId,
+ "description": "Global" if not mandateId else f"Mandate: {mandateId}"
+ },
+ "roles": [r.model_dump() for r in roles],
+ "rules": [r.model_dump() for r in rules]
+ }
+```
+
+### 8.3 Import-Endpoint
+
+```python
+@router.post("/import")
+async def importRbac(
+ data: RbacImportRequest,
+ mode: str = Query("merge", description="merge oder replace"),
+ currentUser: User = Depends(getCurrentUser)
+):
+ """
+ Importiert RBAC-Konfiguration.
+ - mode=merge: Upsert (bestehende werden aktualisiert)
+ - mode=replace: Alle bestehenden Regeln des Scope löschen, dann importieren
+ """
+ interface = getInterface(currentUser)
+ mandateId = data.scope.get("mandateId")
+
+ # Nur sysadmin darf importieren
+ if not _isSysAdmin(currentUser):
+ raise HTTPException(403, "Only sysadmin can import RBAC")
+
+ if mode == "replace" and mandateId:
+ # Lösche alle bestehenden Regeln für diesen Mandanten
+ interface.deleteAccessRulesByMandate(mandateId)
+
+ imported = {"roles": 0, "rules": 0}
+
+ # Rollen importieren
+ for roleData in data.roles:
+ existing = interface.getRoleByLabel(roleData["roleLabel"])
+ if existing:
+ interface.updateRole(existing.id, Role(**roleData))
+ else:
+ interface.createRole(Role(**roleData))
+ imported["roles"] += 1
+
+ # Regeln importieren
+ for ruleData in data.rules:
+ ruleData["mandateId"] = mandateId # Scope setzen
+
+ # Eindeutigkeit: roleLabel + context + item + mandateId
+ existing = interface.findAccessRule(
+ roleLabel=ruleData["roleLabel"],
+ context=ruleData["context"],
+ item=ruleData.get("item"),
+ mandateId=mandateId
+ )
+
+ if existing:
+ interface.updateAccessRule(existing.id, AccessRule(**ruleData))
+ else:
+ interface.createAccessRule(AccessRule(**ruleData))
+ imported["rules"] += 1
+
+ return {"status": "success", "imported": imported}
+```
+
+---
+
+## 9. Migration
+
+### 9.1 Datenbank-Migration
+
+```python
+# migration_001_user_mandate.py
+
+def migrate():
+ """
+ Migriert bestehende User.mandateId zu UserMandate-Einträgen.
+ """
+ db = getRootDbConnector()
+
+ # 1. UserMandate-Tabelle erstellen
+ db._ensureTableExists(UserMandate)
+
+ # 2. Bestehende User migrieren
+ users = db.getRecordset(UserInDB)
+
+ for user in users:
+ mandateId = user.get("mandateId")
+ if mandateId:
+ # Prüfen ob Membership existiert
+ existing = db.getRecordset(
+ UserMandate,
+ recordFilter={"userId": user["id"], "mandateId": mandateId}
+ )
+
+ if not existing:
+ # Membership erstellen
+ membership = UserMandate(
+ userId=user["id"],
+ mandateId=mandateId,
+ roleLabels=user.get("roleLabels", ["user"]),
+ enabled=True,
+ acceptedAt=getUtcTimestamp()
+ )
+ db.recordCreate(UserMandate, membership)
+ print(f"Created membership for user {user['id']} in mandate {mandateId}")
+
+ # 3. Optional: mandateId und roleLabels aus User entfernen
+ # (erst nach vollständigem Test der neuen Logik)
+
+ print("Migration completed")
+```
+
+### 9.2 Abwärtskompatibilität
+
+Während der Übergangsphase:
+
+```python
+def getCurrentUser(token: str = Depends(cookieAuth)) -> User:
+ """
+ Lädt User und setzt Legacy-Kompatibilität.
+ """
+ user = _loadUserFromToken(token)
+
+ # Legacy: Falls Code noch user.mandateId erwartet
+ # Erstes Mandat aus Memberships als Default
+ memberships = getUserMandates(user.id)
+ if memberships:
+ user.mandateId = memberships[0].mandateId # Deprecated
+ user.roleLabels = memberships[0].roleLabels # Deprecated
+
+ return user
+```
+
+---
+
+## 9. Schrittweise Umsetzung
+
+### Phase 1: Datenmodell (Woche 1-2)
+- [ ] `UserMandate` Model erstellen
+- [ ] `AccessRule.mandateId` hinzufügen
+- [ ] `User` Model anpassen (transiente Felder)
+- [ ] Migrations-Script erstellen
+
+### Phase 2: Backend-Core (Woche 2-3)
+- [ ] `interfaceDbAppObjects` Membership-Methoden
+- [ ] `interfaceRbac` mandateId-Parameter
+- [ ] `RbacClass` mandatsspezifische Regeln
+- [ ] Auth-Token ohne mandateId
+
+### Phase 3: API-Routen (Woche 3-4)
+- [ ] Membership-Routen erstellen
+- [ ] Bestehende Routen anpassen (mandateId explizit)
+- [ ] RBAC Import/Export Endpoints
+
+### Phase 4: Tests & Migration (Woche 4-5)
+- [ ] Unit-Tests für Memberships
+- [ ] Integration-Tests für RBAC
+- [ ] Migration bestehender Daten
+- [ ] Abwärtskompatibilität testen
+
+### Phase 5: Frontend (Woche 5-6)
+- [ ] Mandanten-Auswahl UI
+- [ ] Membership-Verwaltung
+- [ ] RBAC Import/Export UI
+
+---
+
+## 11. Betroffene Dateien
+
+### 11.1 Zu ändern
+
+| Datei | Änderungen |
+|-------|------------|
+| `datamodels/datamodelUam.py` | `UserMandate` Model, `User` anpassen |
+| `datamodels/datamodelRbac.py` | `AccessRule.mandateId` |
+| `auth/authentication.py` | Token ohne mandateId |
+| `security/rbac.py` | Mandant-aware Regelauflösung |
+| `interfaces/interfaceRbac.py` | mandateId-Parameter |
+| `interfaces/interfaceDbAppObjects.py` | Membership-Methoden, Kontext-Logik |
+| `interfaces/interfaceDbTrusteeObjects.py` | Mandant aus Request statt User |
+| `interfaces/interfaceBootstrap.py` | Initiale Memberships |
+| `routes/routeDataUsers.py` | mandateId explizit |
+| `routes/routeDataMandates.py` | Membership-Filter |
+| `routes/routeRbac.py` | Import/Export Endpoints |
+
+### 11.2 Neu zu erstellen
+
+| Datei | Beschreibung |
+|-------|--------------|
+| `routes/routeMandateMemberships.py` | Membership-API |
+| `migrations/001_user_mandate.py` | Datenmigration |
+
+### 11.3 Tests anzupassen
+
+- `tests/unit/rbac/*`
+- `tests/integration/rbac/*`
+- `tests/functional/test*`
+
+---
+
+## 12. Risiken & Mitigationen
+
+| Risiko | Mitigation |
+|--------|------------|
+| Breaking Changes | Übergangsphase mit Legacy-Kompatibilität |
+| Datenverlust | Migration erstellt Memberships, löscht nichts |
+| Performance | Membership-Cache pro Request |
+| Komplexität | Schrittweise Umsetzung, Feature-Flags |
+
+---
+
+## 13. Geklärte Design-Entscheidungen
+
+1. **Frontend-Handling:** Features als Objekte/Gruppen organisiert, generisches Handling für Instanzen. Berechtigungen werden **summarisch** pro Feature-Instanz geladen (ein Request), nicht pro Objekt/Feld. → Siehe [ui_concept_nyla.md](./ui_concept_nyla.md)
+
+2. **Default-Mandant:** User hat **keinen** Default-Mandanten. User ohne Mandant-Zugehörigkeit kann System-Basics nutzen (Login, Profil, Settings). Ein sysadmin hat typischerweise keinen Mandanten.
+
+3. **Einladungs-Flow:** Email-Link mit überwachtem Token. Admin kann Token jederzeit widerrufen/löschen. Token hat Ablaufzeit.
+
+4. **System-Rollen (sysadmin):** Global, nicht mandantsspezifisch. sysadmin wird direkt am User-Objekt markiert, nicht über Membership.
diff --git a/implementation/Saas Multi Tenant Mandate/mandate_concept.md b/implementation/Saas Multi Tenant Mandate/mandate_concept.md
new file mode 100644
index 0000000..e187bf5
--- /dev/null
+++ b/implementation/Saas Multi Tenant Mandate/mandate_concept.md
@@ -0,0 +1,609 @@
+# Multi-Tenant SaaS Plattform
+
+## Architektur-Konzept für Mandantenfähigkeit
+
+**Version:** 1.0
+
+**Datum:** 14. Januar 2025
+
+**Status:** Konzept
+
+---
+
+## 1. Executive Summary
+
+### Was ist das Ziel?
+
+Wir entwickeln eine **mandantenfähige SaaS-Plattform**, die es verschiedenen Organisationen (Mandanten) ermöglicht, mehrere Geschäftsanwendungen (Features) über ein einziges System zu nutzen, während:
+
+- ✅ **Datenisolation** zwischen Mandanten gewährleistet ist
+- ✅ **Flexible Zugriffskontrolle** auf Daten- und Feldebene möglich ist
+- ✅ **Ein Benutzer mit einem Account** in mehreren Mandanten arbeiten kann
+- ✅ **Self-Service-Onboarding** für neue Benutzer unterstützt wird
+
+### Hauptmerkmale
+
+| Merkmal | Beschreibung |
+| --- | --- |
+| **Multi-Mandant** | Verschiedene Firmen nutzen die gleiche Plattform isoliert |
+| **Multi-Feature** | Pro Mandant können verschiedene Geschäftsanwendungen aktiviert werden |
+| **Ein Account, viele Rollen** | Ein Benutzer kann in verschiedenen Mandanten unterschiedliche Rollen haben |
+| **Granulare Berechtigungen** | Zugriffskontrolle bis auf Feld-Ebene möglich |
+| **Konfigurierbar** | Berechtigungen können als Konfiguration importiert/exportiert werden |
+
+---
+
+## 2. Geschäftsziele & Nutzen
+
+### 2.1 Für die Plattform-Betreiber
+
+**Effizienz:**
+
+- Ein System für alle Kunden (Mandanten)
+- Zentrale Wartung und Updates
+- Reduzierte Infrastruktur-Kosten
+
+**Skalierbarkeit:**
+
+- Beliebig viele Mandanten onboardbar
+- Neue Features können zentral ausgerollt werden
+- Pay-per-Use Modelle möglich
+
+**Compliance:**
+
+- Strikte Datentrennung zwischen Mandanten
+- Audit-fähige Zugriffskontrollen
+- DSGVO-konform durch granulare Berechtigungen
+
+### 2.2 Für Mandanten (Kunden)
+
+**Flexibilität:**
+
+- Nur die Features nutzen, die benötigt werden
+- Benutzer können in mehreren Mandanten aktiv sein
+- Anpassbare Berechtigungsstrukturen
+
+**Effizienz:**
+
+- Schnelles Onboarding neuer Mitarbeiter
+- Self-Service Einladungssystem
+- Zentrale Benutzerverwaltung
+
+**Sicherheit:**
+
+- Eigene Daten sind isoliert von anderen Mandanten
+- Granulare Kontrolle über Datenzugriffe
+- Nachvollziehbare Berechtigungsstruktur
+
+### 2.3 Für End-Benutzer
+
+**Einfachheit:**
+
+- Ein Login für alle Mandanten
+- Einfacher Wechsel zwischen Mandanten
+- Klare Rollenzuordnungen
+
+**Transparenz:**
+
+- User sieht, in welchen Mandanten er Mitglied ist
+- Klare Rollen und Berechtigungen
+- Keine versteckten Zugriffe
+
+---
+
+## 3. Kernkonzepte
+
+### 3.1 Was ist ein Mandant?
+
+Ein **Mandant** ist eine Organisation (z.B. eine Firma), die die Plattform nutzt. Jeder Mandant hat:
+
+- Eigene Benutzer
+- Eigene Daten
+- Eigene Feature-Instanzen
+
+**Beispiele:**
+
+- Treuhandbüro "Soha Treuhand"
+- Beratungsfirma "Althaus Consulting"
+- Immobilienfirma "Universe Corp"
+
+### 3.2 Was ist ein Feature?
+
+Ein **Feature** ist eine Geschäftsanwendung innerhalb der Plattform. Beispiele:
+
+| Feature | Beschreibung | Typische Nutzer |
+| --- | --- | --- |
+| **Chatbot** | KI-gestützte Chatbots für verschiedene Anwendungsfälle | Kundenservice, Support |
+| **Treuhand** | Buchhaltung und Treuhandverwaltung | Treuhänder, Kunden |
+| **Immobilien** | Verwaltung von Immobilienprojekten | Property Manager, Mieter |
+
+### 3.3 Was ist eine Feature-Instanz?
+
+Ein Mandant kann **mehrere Instanzen** desselben Features haben:
+
+**Beispiel: Mandant "Althaus"**
+
+- Chatbot-Instanz "Produkt-Support" (für Produktfragen)
+- Chatbot-Instanz "HR-Assistent" (für HR-Anfragen)
+- Chatbot-Instanz "Management-Tool" (für Strategiediskussionen)
+
+Jede Instanz hat eigene Daten und Benutzer-Berechtigungen.
+
+### 3.4 Benutzer-Zuordnung: Das "Ein Account, viele Rollen"-Prinzip
+
+```
+Benutzerin: Shelly (shelly@universe.com)
+│
+├─ Mandant: Althaus
+│ ├─ Rolle im Mandant: Standard-Benutzer
+│ └─ Zugriff auf:
+│ ├─ Chatbot "Produkt-Support" → Rolle: Administrator
+│ └─ Chatbot "HR-Assistent" → Rolle: Benutzer
+│
+├─ Mandant: Soha Treuhand
+│ ├─ Rolle im Mandant: Standard-Benutzer
+│ └─ Zugriff auf:
+│ └─ Treuhand "Buchhaltung 2025" → Rolle: Kunde (nur eigene Daten)
+│
+└─ Mandant: Universe Corp
+ ├─ Rolle im Mandant: Mandanten-Administrator
+ └─ Zugriff auf:
+ └─ Alle Features im Mandant
+
+```
+
+**Ergebnis:** Shelly hat **einen Account**, aber **verschiedene Rollen** in **drei Mandanten**.
+
+---
+
+## 4. Benutzerrollen & Verantwortlichkeiten
+
+### 4.1 System-Administrator
+
+**Wer:** Plattform-Betreiber (z.B. IT-Team)
+
+**Verantwortlichkeiten:**
+
+- ✅ Erstellt und verwaltet Mandanten
+- ✅ Schaltet Features für Mandanten frei
+- ✅ Weist Benutzer zu Mandanten zu
+- ✅ Definiert und verwaltet Berechtigungs-Policies
+- ✅ Hat Zugriff auf alle Daten (für Support/Wartung)
+
+**Kann NICHT:**
+
+- ❌ Automatisch in Feature-Instanzen arbeiten (muss sich explizit Zugriff geben)
+
+### 4.2 Mandanten-Administrator
+
+**Wer:** Führungsperson oder IT-Verantwortlicher der Organisation
+
+**Verantwortlichkeiten:**
+
+- ✅ Erstellt Feature-Instanzen im eigenen Mandant
+- ✅ Erstellt Einladungslinks für neue Benutzer
+- ✅ Vergibt Rollen innerhalb des Mandanten
+- ✅ Sieht alle Benutzer des eigenen Mandanten
+- ✅ Verwaltet Feature-Instanzen
+
+**Kann NICHT:**
+
+- ❌ Benutzer zum Mandanten hinzufügen (nur System-Admin)
+- ❌ Features für den Mandanten freischalten (nur System-Admin)
+- ❌ Andere Mandanten sehen oder darauf zugreifen
+
+**Isolation:** Ein Mandanten-Administrator sieht nur die Benutzer und Daten seines eigenen Mandanten.
+
+### 4.3 Standard-Benutzer
+
+**Wer:** Mitarbeiter, Kunden, Partner der Organisation
+
+**Verantwortlichkeiten:**
+
+- ✅ Nutzt Feature-Instanzen entsprechend seiner Rollen
+- ✅ Arbeitet mit Daten im Rahmen seiner Berechtigungen
+
+**Kann NICHT:**
+
+- ❌ Feature-Instanzen erstellen
+- ❌ Andere Benutzer verwalten
+- ❌ Berechtigungen vergeben
+
+### 4.4 Rollen innerhalb von Features
+
+Jede Feature-Instanz hat eigene Rollen:
+
+| Rolle | Typische Berechtigungen | Beispiel |
+| --- | --- | --- |
+| **Administrator** | Voller Zugriff auf alle Daten der Instanz | Chatbot-Admin sieht alle Konversationen |
+| **Benutzer** | Zugriff auf Instanz-Daten, kann eigene Daten erstellen | Marketing-Mitarbeiter nutzt Chatbot |
+| **Kunde** | Nur Zugriff auf eigene Daten | Treuhand-Kunde sieht nur eigene Buchhaltung |
+| **Betrachter** | Nur Lesezugriff | Externer Berater sieht Berichte, kann nichts ändern |
+
+---
+
+## 5. Anwendungsfälle (Use Cases)
+
+### 5.1 Use Case 1: Patrick mit mehreren Firmen
+
+**Situation:**
+Patrick hat 3 eigene Firmen und nutzt für jede ein anderes Treuhandbüro. Gleichzeitig arbeitet er als externer Berater für die Firma Althaus.
+
+**Lösung:**
+
+```
+Benutzer: Patrick (patrick@example.com)
+│
+├─ Mandant: Soha Treuhand (Treuhandbüro 1)
+│ └─ Treuhand-Instanz "PamoCreate AG" → Rolle: Kunde
+│ └─ Treuhand-Instanz "ValueOn AG" → Rolle: Kunde
+│ └─ Treuhand-Instanz "PowerOn GmbH" → Rolle: Kunde
+│
+├─ Mandant: SwissTreu (Treuhandbüro 2)
+│ └─ Treuhand-Instanz "Firma X" → Rolle: Kunde
+│
+└─ Mandant: Althaus (Beratungsfirma)
+ └─ Chatbot-Instanz "Management-Tool" → Rolle: Administrator
+
+```
+
+**Vorteile:**
+
+- Patrick hat **einen Login** für alle Mandanten
+- Patrick sieht bei Soha Treuhand nur seine 3 Firmen (nicht die Daten anderer Kunden)
+- Patrick ist bei Althaus Administrator und kann dort alles verwalten
+- Jeder Mandant sieht nur seine eigenen Daten
+
+### 5.2 Use Case 2: Shelly als Multi-Mandant-Benutzer
+
+**Situation:**
+Shelly arbeitet für ihre eigene Firma "Universe Corp", unterstützt aber auch andere Firmen als Freelancerin.
+
+**Lösung:**
+
+```
+Benutzerin: Shelly (shelly@universe.com)
+│
+├─ Mandant: Universe Corp (eigene Firma)
+│ ├─ Rolle: Mandanten-Administrator
+│ └─ Kann alle Features verwalten und Benutzer einladen
+│
+├─ Mandant: Althaus (Kunde als Freelancerin)
+│ └─ Chatbot-Instanz "Produkt-Support" → Rolle: Administrator
+│ └─ Chatbot-Instanz "HR-Assistent" → Rolle: Benutzer (nur für einen spezifischen Bot)
+│
+└─ Mandant: Soha Treuhand (nutzt deren Treuhand-Service)
+ └─ Treuhand-Instanz "Universe Corp 2025" → Rolle: Kunde
+
+```
+
+**Vorteile:**
+
+- Shelly verwaltet ihren eigenen Mandanten vollständig
+- Shelly kann bei Althaus als Expertin unterstützen
+- Shelly nutzt gleichzeitig Soha Treuhand als Kundin
+- Klare Rollentrennung in jedem Mandanten
+
+### 5.3 Use Case 3: Treuhandbüro mit vielen Kunden
+
+**Situation:**
+Das Treuhandbüro "Soha Treuhand" betreut 50 kleine Firmen. Jede Firma soll nur ihre eigenen Buchhaltungsdaten sehen.
+
+**Lösung:**
+
+```
+Mandant: Soha Treuhand
+│
+├─ Feature: Treuhand (aktiviert)
+│
+├─ Feature-Instanz: "Buchhaltung 2025"
+│ │
+│ ├─ Mitarbeiter "Anna" → Rolle: Administrator (sieht alle Kunden)
+│ ├─ Mitarbeiter "Tom" → Rolle: Benutzer (sieht alle Kunden)
+│ │
+│ ├─ Kunde "Patrick (PamoCreate AG)" → Rolle: Kunde (sieht nur PamoCreate)
+│ ├─ Kunde "Shelly (Universe Corp)" → Rolle: Kunde (sieht nur Universe)
+│ └─ ... 48 weitere Kunden ...
+
+```
+
+**Datenzugriff:**
+
+- **Anna** (Administrator): Sieht alle 50 Firmen und deren Daten
+- **Tom** (Benutzer): Sieht alle 50 Firmen und deren Daten
+- **Patrick** (Kunde): Sieht **nur** Daten von PamoCreate AG
+- **Shelly** (Kunde): Sieht **nur** Daten von Universe Corp
+
+**Technische Umsetzung:** Automatische Filterung basierend auf "Eigentümer"-Feld in der Datenbank.
+
+---
+
+## 6. Datensicherheit & Isolation
+
+### 6.1 Mandanten-Isolation
+
+**Prinzip:** Jeder Mandant ist vollständig von anderen Mandanten getrennt.
+
+**Konkret:**
+
+- Mandant A sieht **keine Daten** von Mandant B
+- Mandant A sieht **keine Benutzer** von Mandant B
+- Mandant A kann **nicht auf Feature-Instanzen** von Mandant B zugreifen
+
+**Ausnahme:** System-Administrator (für Support und Wartung)
+
+### 6.2 Zugriffskontrolle: Das "n/m/g/a"-System
+
+Für jede Rolle in einer Feature-Instanz wird der **Datenzugriff** definiert:
+
+| Scope | Name | Beschreibung | Beispiel |
+| --- | --- | --- | --- |
+| **n** | None | Kein Zugriff | Kunde sieht keine Admin-Einstellungen |
+| **m** | My | Nur eigene Daten | Kunde sieht nur eigene Rechnungen |
+| **g** | Group | Alle Daten der Instanz | Mitarbeiter sieht alle Kunden der Instanz |
+| **a** | All | Alle Daten des Features | Super-Admin sieht alle Instanzen |
+
+**Anwendungsbeispiel:**
+
+```
+Feature: Chatbot
+Instanz: "Produkt-Support"
+
+Rolle "Kunde":
+ - Konversationen: Scope "m" (my) → Nur eigene Chats
+ - Einstellungen: Scope "n" (none) → Kein Zugriff
+
+Rolle "Benutzer":
+ - Konversationen: Scope "g" (group) → Alle Chats der Instanz
+ - Einstellungen: Scope "n" (none) → Kein Zugriff
+
+Rolle "Administrator":
+ - Konversationen: Scope "g" (group) → Alle Chats der Instanz
+ - Einstellungen: Scope "g" (group) → Voller Zugriff auf Einstellungen
+
+```
+
+### 6.3 Feld-Level-Sicherheit
+
+Zusätzlich zur Tabellen-Ebene können einzelne **Datenfelder** geschützt werden:
+
+**Beispiel: Benutzer-Tabelle**
+
+| Feld | System-Admin | Mandanten-Admin | Standard-Benutzer |
+| --- | --- | --- | --- |
+| Name | Lesen/Schreiben | Lesen | Lesen (nur eigener) |
+| Email | Lesen/Schreiben | Lesen | Lesen/Schreiben (nur eigene) |
+| Passwort-Hash | Lesen/Schreiben | Kein Zugriff | Kein Zugriff |
+| isSystemAdmin | Lesen/Schreiben | Kein Zugriff | Kein Zugriff |
+
+**Vorteil:** Sehr granulare Kontrolle über sensible Daten.
+
+### 6.4 Berechtigungs-Policies (Regelsets)
+
+**Was sind Policies?**
+Policies sind **konfigurierbare Regelsets**, die definieren, wer auf welche Daten zugreifen kann.
+
+**Beispiel-Policy: "Chatbot Kunde"**
+
+```
+Policy-Name: chatbot_customer_policy
+Gilt für: Rolle "Kunde" im Feature "Chatbot"
+
+Regeln:
+1. Tabelle "Konversationen" → Lesen erlaubt (Scope: my)
+2. Tabelle "Konversationen" → Erstellen erlaubt (Scope: my)
+3. Tabelle "Nachrichten" → Lesen erlaubt (Scope: my)
+4. Tabelle "Admin-Einstellungen" → Kein Zugriff (Scope: none)
+
+```
+
+**Vorteile:**
+
+- ✅ Policies können als Konfiguration exportiert/importiert werden
+- ✅ Änderungen benötigen keine Code-Anpassungen
+- ✅ Nachvollziehbar und audit-fähig
+- ✅ Wiederverwendbar über verschiedene Mandanten
+
+---
+
+## 7. Onboarding-Prozesse
+
+### 7.1 Neuer Mandant wird erstellt
+
+```
+1. System-Administrator erstellt Mandant
+ └─ Mandant-Name: "Althaus"
+ └─ Mandant-Typ: "Enterprise"
+
+2. System-Administrator schaltet Features frei
+ └─ Feature "Chatbot": Aktiviert
+ └─ Feature "Treuhand": Nicht aktiviert
+
+3. System-Administrator weist ersten Benutzer als Mandanten-Admin zu
+ └─ User: admin@althaus.com
+ └─ Rolle: Mandanten-Administrator
+
+4. Mandanten-Administrator übernimmt
+ └─ Erstellt Feature-Instanzen
+ └─ Lädt weitere Benutzer ein
+
+```
+
+### 7.2 Neuer Benutzer wird eingeladen (Self-Service)
+
+```
+1. Mandanten-Administrator erstellt Einladungslink
+ └─ Feature-Instanz: "Chatbot Produkt-Support"
+ └─ Rolle: "Benutzer"
+ └─ Link: https://app.example.com/register?invite=abc123
+ └─ Gültigkeit: 24 Stunden
+
+2. Administrator sendet Link an neue Mitarbeiterin
+
+3. Neue Mitarbeiterin öffnet Link und registriert sich
+ └─ Benutzername: "anna"
+ └─ Email: "anna@althaus.com"
+ └─ Passwort: ***
+
+4. System erstellt automatisch:
+ └─ Benutzer-Account
+ └─ Zuordnung zum Mandanten "Althaus"
+ └─ Rolle "Benutzer" in "Chatbot Produkt-Support"
+
+5. Anna kann sich sofort einloggen und arbeiten
+
+```
+
+### 7.3 Bestehender Benutzer wird zu weiterem Mandanten hinzugefügt
+
+```
+1. System-Administrator weist Shelly zu neuem Mandanten zu
+ └─ User: shelly@universe.com (besteht bereits)
+ └─ Neuer Mandant: "Soha Treuhand"
+ └─ Rolle: "Standard-Benutzer"
+
+2. Mandanten-Admin von "Soha Treuhand" vergibt Feature-Rolle
+ └─ Feature-Instanz: "Buchhaltung 2025"
+ └─ Rolle: "Kunde"
+
+3. Shelly erhält Email-Benachrichtigung
+
+4. Shelly loggt sich ein und sieht neuen Mandanten im Menü
+ └─ Kann zwischen "Universe Corp" und "Soha Treuhand" wechseln
+
+```
+
+---
+
+## 8. Skalierbarkeit & Flexibilität
+
+### 8.1 Skalierung von Mandanten
+
+**Horizontal:**
+
+- Beliebig viele Mandanten können hinzugefügt werden
+- Jeder Mandant ist isoliert → keine Auswirkungen auf andere
+- Neue Mandanten können in Minuten onboardet werden
+
+**Vertikal:**
+
+- Pro Mandant können beliebig viele Feature-Instanzen erstellt werden
+- Pro Feature-Instanz können beliebig viele Benutzer hinzugefügt werden
+
+### 8.2 Neue Features hinzufügen
+
+```
+1. Entwicklung eines neuen Features (z.B. "CRM")
+
+2. System-Administrator erstellt Feature-Definition
+ └─ Feature-Code: "crm"
+ └─ Verfügbare Rollen: ["admin", "user", "viewer"]
+
+3. System-Administrator importiert Berechtigungs-Policies
+ └─ Import von JSON-Konfiguration
+
+4. System-Administrator schaltet Feature für Mandanten frei
+ └─ Mandant "Althaus" → CRM aktiviert
+ └─ Mandant "Soha Treuhand" → CRM nicht aktiviert
+
+5. Mandanten-Administratoren können Feature nutzen
+ └─ Erstellen CRM-Instanzen
+ └─ Laden Benutzer ein
+
+```
+
+**Vorteil:** Neue Features können zentral entwickelt und flexibel ausgerollt werden.
+
+### 8.3 Anpassbare Berechtigungen
+
+**Szenario:** Mandant "Althaus" benötigt spezielle Berechtigungsstruktur.
+
+**Lösung:**
+
+1. System-Administrator exportiert Standard-Policies
+2. Mandanten-Administrator passt Policies an (mit Freigabe)
+3. System-Administrator importiert angepasste Policies für diesen Mandanten
+4. Nur "Althaus" nutzt angepasste Policies, andere Mandanten Standard
+
+**Vorteil:** Flexibilität für Sonderanforderungen ohne Code-Änderungen.
+
+---
+
+## 9. Frequently Asked Questions (FAQ)
+
+### F: Kann ein Benutzer wirklich mit einem Account in mehreren Mandanten arbeiten?
+
+**A:** Ja, das ist ein Kernfeature. Ein Benutzer hat einen globalen Account und kann in beliebig vielen Mandanten Mitglied sein. In jedem Mandanten kann er unterschiedliche Rollen haben.
+
+### F: Was passiert, wenn zwei Mandanten den gleichen Benutzer einladen wollen?
+
+**A:** Da Benutzernamen eindeutig sind, Email-Adressen aber nicht, kann ein Benutzer:
+
+- Entweder seinen bestehenden Account nutzen (System-Admin weist zu)
+- Oder ein zweiter Account wird mit anderem Benutzernamen erstellt
+
+**Empfehlung:** System-Admin weist bestehenden Account zu, sodass ein Benutzer wirklich nur einen Account benötigt.
+
+### F: Können Mandanten-Administratoren Benutzer von anderen Mandanten sehen?
+
+**A:** Nein, strikte Isolation. Ein Mandanten-Admin sieht nur:
+
+- Benutzer seines eigenen Mandanten
+- Feature-Instanzen seines eigenen Mandanten
+- Daten seines eigenen Mandanten
+
+### F: Wie wird sichergestellt, dass Daten zwischen Mandanten getrennt bleiben?
+
+**A:** Mehrschichtige Sicherheit:
+
+1. **Architektur-Ebene:** Jede Datenabfrage filtert automatisch nach Mandant
+2. **Berechtigungs-Ebene:** Policies prüfen Zugriff auf Tabellen-/Feldebene
+3. **Datenbank-Ebene:** Row-Level Security kann zusätzlich aktiviert werden
+
+### F: Was ist der Unterschied zwischen "Mandanten-Admin" und "Feature-Administrator"?
+
+**A:**
+
+- **Mandanten-Admin:** Verwaltet den gesamten Mandanten (erstellt Feature-Instanzen, lädt Benutzer ein)
+- **Feature-Administrator:** Hat Admin-Rolle in einer spezifischen Feature-Instanz (z.B. Chatbot-Admin)
+
+Ein Benutzer kann beides gleichzeitig sein.
+
+### F: Können Berechtigungen für einzelne Benutzer individuell angepasst werden?
+
+**A:** Ja, über "Custom Permissions". Standard sind Rollen-basierte Policies, aber für Sonderfälle können individuelle Überschreibungen definiert werden.
+
+### F: Wie werden neue Features ausgerollt?
+
+**A:** Zentral und kontrolliert:
+
+1. Feature wird entwickelt
+2. System-Admin schaltet Feature für ausgewählte Mandanten frei
+3. Mandanten-Admins können Feature-Instanzen erstellen
+4. Schrittweises Rollout möglich (Beta-Mandanten zuerst)
+
+### F: Was passiert, wenn ein Mandant gekündigt wird?
+
+**A:**
+
+1. System-Admin deaktiviert Mandanten
+2. Alle Benutzer verlieren Zugriff auf diesen Mandanten
+3. Daten können archiviert oder gelöscht werden (nach DSGVO)
+4. Benutzer, die nur in diesem Mandanten waren, verlieren Zugriff
+5. Benutzer, die in anderen Mandanten sind, behalten diese Zugriffe
+
+---
+
+## 10. Glossar
+
+| Begriff | Erklärung |
+| --- | --- |
+| **Mandant** | Organisation (Firma), die die Plattform nutzt |
+| **Feature** | Geschäftsanwendung (z.B. Chatbot, Treuhand, CRM) |
+| **Feature-Instanz** | Konkrete Instanz eines Features in einem Mandanten |
+| **System-Administrator** | Plattform-Betreiber mit globalen Rechten |
+| **Mandanten-Administrator** | Verwalter eines spezifischen Mandanten |
+| **Policy** | Konfigurierbare Berechtigungsregel |
+| **Scope (n/m/g/a)** | Datenzugriffs-Bereich (None/My/Group/All) |
+| **Isolation** | Strikte Trennung zwischen Mandanten |
+| **Onboarding** | Prozess der Neukundenaufnahm |
\ No newline at end of file
diff --git a/implementation/Saas Multi Tenant Mandate/ui_concept_nyla.md b/implementation/Saas Multi Tenant Mandate/ui_concept_nyla.md
new file mode 100644
index 0000000..f414850
--- /dev/null
+++ b/implementation/Saas Multi Tenant Mandate/ui_concept_nyla.md
@@ -0,0 +1,880 @@
+# Multi-Tenant UI Konzept - Frontend Nyla
+
+## Architektur für mandantenunabhängige Benutzer
+
+**Version:** 1.0
+**Datum:** 14. Januar 2026
+**Status:** Entwurf
+**Frontend:** Nyla (React)
+
+---
+
+## 1. Grundprinzip
+
+### 1.1 User ohne Mandanten-Zugehörigkeit
+
+Ein User gehört **keinem Mandanten** an. Er sieht:
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ User: patrick@example.com │
+│ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ SYSTEM (immer verfügbar) │ │
+│ │ • Profil bearbeiten │ │
+│ │ • Einstellungen │ │
+│ │ • Abmelden │ │
+│ └─────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ FEATURE: Trustee │ │
+│ │ ├─ Instanz: "Soha Treuhand / PamoCreate AG" │ │
+│ │ ├─ Instanz: "Soha Treuhand / ValueOn AG" │ │
+│ │ └─ Instanz: "SwissTreu / Firma X" │ │
+│ └─────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────┐ │
+│ │ FEATURE: Chatbot │ │
+│ │ └─ Instanz: "Althaus / Management-Tool" │ │
+│ └─────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 1.2 Keine mandateId im Frontend
+
+**Alt:** Frontend speicherte `mandateId` im State und sendete bei jedem Request.
+
+**Neu:** Frontend kennt nur **Feature-Instanzen**. Der Mandant ergibt sich aus der Instanz.
+
+```typescript
+// ALT
+const currentMandateId = useStore(state => state.mandateId);
+await api.get('/trustee/contracts', { params: { mandateId: currentMandateId }});
+
+// NEU
+const currentInstance = useStore(state => state.currentFeatureInstance);
+await api.get('/trustee/contracts', { params: { instanceId: currentInstance.id }});
+// Backend ermittelt mandateId aus der Instanz
+```
+
+---
+
+## 2. Feature-Objekt-Struktur
+
+### 2.1 Feature als UI-Gruppe
+
+Jedes Feature ist ein **Objekt**, das im UI als Gruppe organisiert ist:
+
+```typescript
+interface Feature {
+ code: string; // "trustee", "chatbot", "crm"
+ label: I18nLabel; // { en: "Trustee", de: "Treuhand" }
+ icon: string; // Material Icon Name
+ instances: FeatureInstance[];
+}
+
+interface FeatureInstance {
+ id: string; // UUID der Instanz
+ featureCode: string; // "trustee"
+ mandateId: string; // Referenz (für Backend)
+ mandateName: string; // "Soha Treuhand" (für Anzeige)
+ instanceLabel: string; // "PamoCreate AG" (spezifischer Name)
+ userRole: string; // Rolle des Users in dieser Instanz
+ permissions: InstancePermissions; // Summarische Berechtigungen
+}
+```
+
+### 2.2 Generisches Instanz-Handling
+
+Das Handling für Feature-Instanzen ist **generisch**. Die aktuelle Instanz ergibt sich aus der **URL** (Route-Parameter):
+
+```typescript
+// stores/featureStore.ts
+
+interface FeatureState {
+ features: Feature[];
+
+ // Actions
+ loadFeatures: () => Promise;
+ getInstanceById: (instanceId: string) => FeatureInstance | undefined;
+ getFeatureByCode: (featureCode: string) => Feature | undefined;
+}
+
+const useFeatureStore = create((set, get) => ({
+ features: [],
+
+ loadFeatures: async () => {
+ // Ein API-Call lädt alle Features + Instanzen + Permissions
+ const response = await api.get('/features/my');
+ set({ features: response.data });
+ },
+
+ getInstanceById: (instanceId) => {
+ return get().features
+ .flatMap(f => f.instances)
+ .find(i => i.id === instanceId);
+ },
+
+ getFeatureByCode: (featureCode) => {
+ return get().features.find(f => f.code === featureCode);
+ }
+}));
+```
+
+### 2.3 Instanz aus URL-Parameter
+
+Die aktuelle Instanz wird **nicht** im Store gespeichert, sondern aus der URL gelesen:
+
+```typescript
+// hooks/useCurrentInstance.ts
+
+export function useCurrentInstance(): FeatureInstance | undefined {
+ const { instanceId } = useParams<{ instanceId: string }>();
+ const getInstanceById = useFeatureStore(s => s.getInstanceById);
+
+ return instanceId ? getInstanceById(instanceId) : undefined;
+}
+
+// Verwendung in Komponenten:
+function ContractList() {
+ const instance = useCurrentInstance();
+
+ if (!instance) {
+ return ; // Keine Instanz in URL
+ }
+
+ // Arbeite mit instance.permissions, instance.mandateId, etc.
+}
+```
+
+---
+
+## 3. Berechtigungs-Abfrage (Summarisch)
+
+### 3.1 Problem: Rate Limits
+
+**Vermeiden:**
+```typescript
+// SCHLECHT: Ein Request pro Objekt/Feld
+for (const contract of contracts) {
+ const canEdit = await api.get(`/rbac/check?table=Contract&id=${contract.id}&action=update`);
+ const canDelete = await api.get(`/rbac/check?table=Contract&id=${contract.id}&action=delete`);
+}
+```
+
+### 3.2 Lösung: Summarische Berechtigungen
+
+Berechtigungen werden **einmalig pro Feature-Instanz** geladen:
+
+```typescript
+interface InstancePermissions {
+ // Tabellen-Level (CRUD pro Tabelle)
+ tables: {
+ [tableName: string]: {
+ view: boolean;
+ read: AccessLevel; // "n" | "m" | "g" | "a"
+ create: AccessLevel;
+ update: AccessLevel;
+ delete: AccessLevel;
+ }
+ };
+
+ // Feld-Level (nur wo eingeschränkt)
+ fields?: {
+ [tableName: string]: {
+ [fieldName: string]: {
+ read: boolean;
+ write: boolean;
+ }
+ }
+ };
+
+ // View-Level (Navigation)
+ views: {
+ [viewCode: string]: boolean;
+ };
+}
+```
+
+### 3.3 API-Endpunkt für Permissions
+
+```typescript
+// GET /features/my
+// Lädt alles in einem Request
+
+{
+ "features": [
+ {
+ "code": "trustee",
+ "label": { "en": "Trustee", "de": "Treuhand" },
+ "instances": [
+ {
+ "id": "inst-123",
+ "mandateId": "mand-456",
+ "mandateName": "Soha Treuhand",
+ "instanceLabel": "PamoCreate AG",
+ "userRole": "customer",
+ "permissions": {
+ "tables": {
+ "TrusteeOrganisation": { "view": true, "read": "m", "create": "n", "update": "m", "delete": "n" },
+ "TrusteeContract": { "view": true, "read": "m", "create": "n", "update": "n", "delete": "n" },
+ "TrusteeDocument": { "view": true, "read": "m", "create": "m", "update": "m", "delete": "n" }
+ },
+ "views": {
+ "trustee-dashboard": true,
+ "trustee-contracts": true,
+ "trustee-admin": false
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
+```
+
+### 3.4 Permission-Hooks im Frontend
+
+```typescript
+// hooks/usePermissions.ts
+
+export function useTablePermission(tableName: string) {
+ const instance = useCurrentInstance(); // Aus URL-Parameter
+
+ if (!instance) {
+ return { view: false, read: 'n', create: 'n', update: 'n', delete: 'n' };
+ }
+
+ return instance.permissions.tables[tableName] ?? {
+ view: false, read: 'n', create: 'n', update: 'n', delete: 'n'
+ };
+}
+
+export function useCanView(viewCode: string): boolean {
+ const instance = useCurrentInstance(); // Aus URL-Parameter
+ return instance?.permissions.views[viewCode] ?? false;
+}
+
+export function useCanEditRecord(tableName: string, record: any): boolean {
+ const permission = useTablePermission(tableName);
+ const userId = useAuthStore(s => s.user?.id);
+
+ switch (permission.update) {
+ case 'n': return false;
+ case 'm': return record._createdBy === userId;
+ case 'g': return true; // Instanz-Scope
+ case 'a': return true; // Alle
+ default: return false;
+ }
+}
+```
+
+---
+
+## 4. Navigation & UI-Struktur
+
+### 4.1 Hauptnavigation
+
+```
+┌────────────────────────────────────────────────────────────┐
+│ [Logo] PowerOn [User] [Logout] │
+├────────────────────────────────────────────────────────────┤
+│ │
+│ SYSTEM │
+│ ○ Dashboard │
+│ ○ Profil │
+│ ○ Einstellungen │
+│ │
+│ ▼ TRUSTEE [Feature] │
+│ │ │
+│ ├─▼ Soha Treuhand / PamoCreate AG [Instanz] │
+│ │ ○ Übersicht │
+│ │ ○ Verträge │
+│ │ ○ Dokumente │
+│ │ ○ Positionen │
+│ │ │
+│ ├─▼ Soha Treuhand / ValueOn AG [Instanz] │
+│ │ ○ Übersicht │
+│ │ ○ Verträge │
+│ │ ○ Dokumente │
+│ │ │
+│ └─▶ SwissTreu / Firma X [Instanz] │
+│ (collapsed) │
+│ │
+│ ▼ CHATBOT [Feature] │
+│ │ │
+│ └─▼ Althaus / Management-Tool [Instanz] │
+│ ○ Conversations │
+│ ○ Settings │
+│ │
+│ ───────────────────────────────────── │
+│ ADMIN (nur wenn sysadmin) │
+│ ○ Mandanten │
+│ ○ Users │
+│ ○ RBAC │
+│ │
+└────────────────────────────────────────────────────────────┘
+```
+
+**Hierarchie:**
+```
+Feature (Gruppe)
+ └─ Instanz (Subgruppe)
+ └─ Objekte/Views (Navigation Items)
+```
+```
+
+### 4.2 Feature-Gruppe mit Instanz-Subgruppen
+
+Jedes Feature zeigt **alle Instanzen als Subgruppen** an:
+
+```tsx
+// components/FeatureNavGroup.tsx
+
+function FeatureNavGroup({ featureCode }: { featureCode: string }) {
+ const { features } = useFeatureStore();
+ const feature = features.find(f => f.code === featureCode);
+
+ if (!feature || feature.instances.length === 0) {
+ return null; // Feature nicht anzeigen wenn keine Instanzen
+ }
+
+ return (
+
+ {/* Feature als Hauptgruppe */}
+
+
+ {feature.label.de}
+
+
+ {/* Jede Instanz als Subgruppe */}
+ {feature.instances.map(instance => (
+
+ ))}
+
+ );
+}
+```
+
+### 4.3 Instanz-Subgruppe Komponente
+
+```tsx
+// components/InstanceNavSubgroup.tsx
+
+function InstanceNavSubgroup({ instance, featureCode }: {
+ instance: FeatureInstance;
+ featureCode: string;
+}) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
+ {/* Instanz als Subgruppen-Header */}
+ setIsExpanded(!isExpanded)}
+ collapsible
+ >
+
+ {instance.mandateName} / {instance.instanceLabel}
+
+
+
+ {/* Objekte/Views der Instanz */}
+ {isExpanded && (
+
+ {instance.permissions.views[`${featureCode}-dashboard`] && (
+
+ Übersicht
+
+ )}
+ {instance.permissions.views[`${featureCode}-contracts`] && (
+
+ Verträge
+
+ )}
+ {instance.permissions.views[`${featureCode}-documents`] && (
+
+ Dokumente
+
+ )}
+ {instance.permissions.views[`${featureCode}-positions`] && (
+
+ Positionen
+
+ )}
+
+ )}
+
+ );
+}
+```
+
+### 4.4 URL-Struktur mit Instanz-ID
+
+Die URL enthält immer die **Instanz-ID**, damit klar ist, in welcher Instanz gearbeitet wird:
+
+```
+/trustee/{instanceId}/dashboard
+/trustee/{instanceId}/contracts
+/trustee/{instanceId}/contracts/{contractId}
+/trustee/{instanceId}/documents
+
+/chatbot/{instanceId}/conversations
+/chatbot/{instanceId}/settings
+```
+
+**Router-Setup:**
+```tsx
+// routes.tsx
+
+}>
+ } />
+ } />
+ } />
+ } />
+ {/* ... */}
+
+```
+
+---
+
+## 5. System-Bereich (Ohne Mandant)
+
+### 5.1 Immer verfügbare Funktionen
+
+User ohne Mandant-Zugehörigkeit können:
+
+```typescript
+const SYSTEM_FEATURES = {
+ // Immer verfügbar
+ profile: true,
+ settings: true,
+ logout: true,
+
+ // Nur für sysadmin
+ adminMandates: (user) => user.isSysAdmin,
+ adminUsers: (user) => user.isSysAdmin,
+ adminRbac: (user) => user.isSysAdmin,
+};
+```
+
+### 5.2 Dashboard für User ohne Instanzen
+
+```tsx
+// pages/Dashboard.tsx
+
+function Dashboard() {
+ const { features } = useFeatureStore();
+ const hasInstances = features.some(f => f.instances.length > 0);
+
+ if (!hasInstances) {
+ return (
+
+ Willkommen bei PowerOn
+ Du hast aktuell Zugriff auf keine Feature-Instanzen.
+ Kontaktiere einen Administrator, um Zugriff zu erhalten.
+
+
+ Was du jetzt tun kannst:
+
+ - Profil bearbeiten
+ - Einstellungen anpassen
+
+
+
+ );
+ }
+
+ return ;
+}
+```
+
+---
+
+## 6. Daten-Requests mit Instanz-Kontext
+
+### 6.1 API-Client mit Instanz-Parameter
+
+Da die Instanz-ID in der URL steht, wird sie **explizit** an API-Calls übergeben:
+
+```typescript
+// services/api.ts
+
+const api = axios.create({ baseURL: '/api' });
+
+// Kein Interceptor nötig - Instanz-ID kommt aus URL und wird explizit übergeben
+```
+
+### 6.2 Feature-spezifische Queries
+
+```typescript
+// hooks/useTrusteeContracts.ts
+
+export function useTrusteeContracts() {
+ const instance = useCurrentInstance(); // Aus URL: /trustee/:instanceId/contracts
+
+ return useQuery({
+ queryKey: ['trustee', 'contracts', instance?.id],
+ queryFn: async () => {
+ if (!instance) throw new Error('No instance in URL');
+
+ // Instanz-ID explizit im Request
+ const response = await api.get(`/trustee/${instance.id}/contracts`);
+ return response.data;
+ },
+ enabled: !!instance && instance.featureCode === 'trustee'
+ });
+}
+```
+
+---
+
+## 7. Berechtigungs-gesteuerte UI-Elemente
+
+### 7.1 Conditional Rendering
+
+```tsx
+// components/ContractList.tsx
+
+function ContractList() {
+ const { data: contracts } = useTrusteeContracts();
+ const tablePermission = useTablePermission('TrusteeContract');
+ const userId = useAuthStore(s => s.user?.id);
+
+ const canCreate = tablePermission.create !== 'n';
+
+ return (
+
+
+ Verträge
+ {canCreate && (
+
+ )}
+
+
+
+ {contracts?.map(contract => {
+ const canEdit = canEditRecord(tablePermission.update, contract, userId);
+ const canDelete = canEditRecord(tablePermission.delete, contract, userId);
+
+ return (
+
+ {contract.name}
+
+ {canEdit && editContract(contract)} />}
+ {canDelete && deleteContract(contract)} />}
+
+
+ );
+ })}
+
+
+ );
+}
+
+function canEditRecord(accessLevel: AccessLevel, record: any, userId: string): boolean {
+ switch (accessLevel) {
+ case 'n': return false;
+ case 'm': return record._createdBy === userId;
+ case 'g':
+ case 'a': return true;
+ default: return false;
+ }
+}
+```
+
+### 7.2 Permission-Wrapper Komponente
+
+```tsx
+// components/PermissionGate.tsx
+
+interface PermissionGateProps {
+ table: string;
+ action: 'view' | 'read' | 'create' | 'update' | 'delete';
+ record?: any; // Für "m" (my) Prüfung
+ children: React.ReactNode;
+ fallback?: React.ReactNode;
+}
+
+function PermissionGate({ table, action, record, children, fallback = null }: PermissionGateProps) {
+ const permission = useTablePermission(table);
+ const userId = useAuthStore(s => s.user?.id);
+
+ let hasPermission = false;
+
+ if (action === 'view') {
+ hasPermission = permission.view;
+ } else {
+ const level = permission[action];
+ if (level === 'n') {
+ hasPermission = false;
+ } else if (level === 'm' && record) {
+ hasPermission = record._createdBy === userId;
+ } else if (level === 'g' || level === 'a') {
+ hasPermission = true;
+ }
+ }
+
+ return hasPermission ? <>{children}> : <>{fallback}>;
+}
+
+// Verwendung:
+
+
+
+
+
+
+
+```
+
+---
+
+## 8. Login & Session
+
+### 8.1 Login-Response (ohne mandateId)
+
+```typescript
+// Nach erfolgreichem Login:
+{
+ "token": "jwt...",
+ "user": {
+ "id": "user-123",
+ "username": "patrick",
+ "email": "patrick@example.com",
+ "isSysAdmin": false
+ },
+ "features": [
+ // Alle Features + Instanzen + Permissions (siehe Abschnitt 3.3)
+ ]
+}
+```
+
+### 8.2 Auth-Store
+
+```typescript
+// stores/authStore.ts
+
+interface AuthState {
+ user: User | null;
+ token: string | null;
+ isAuthenticated: boolean;
+
+ login: (credentials: LoginCredentials) => Promise;
+ logout: () => void;
+}
+
+const useAuthStore = create((set) => ({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+
+ login: async (credentials) => {
+ const response = await api.post('/auth/login', credentials);
+
+ // Token speichern
+ set({
+ user: response.data.user,
+ token: response.data.token,
+ isAuthenticated: true
+ });
+
+ // Features laden (separater Store)
+ useFeatureStore.getState().setFeatures(response.data.features);
+ },
+
+ logout: () => {
+ set({ user: null, token: null, isAuthenticated: false });
+ useFeatureStore.getState().reset();
+ }
+}));
+```
+
+---
+
+## 9. Einladungs-Flow
+
+### 9.1 Einladungs-Link
+
+```
+https://app.poweron.ch/invite?token=abc123xyz
+```
+
+### 9.2 Einladungs-Seite
+
+```tsx
+// pages/Invite.tsx
+
+function InvitePage() {
+ const token = useSearchParam('token');
+ const [inviteData, setInviteData] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ // Token validieren (ohne Login)
+ api.get(`/invite/validate?token=${token}`)
+ .then(res => setInviteData(res.data))
+ .catch(err => setError(err.response?.data?.detail || 'Ungültiger Link'));
+ }, [token]);
+
+ if (error) {
+ return ;
+ }
+
+ if (!inviteData) {
+ return ;
+ }
+
+ return (
+
+ Einladung zu {inviteData.mandateName}
+ Du wurdest eingeladen, als {inviteData.roleLabel} mitzuarbeiten.
+
+ {inviteData.existingUser ? (
+ // User existiert bereits - nur Login erforderlich
+ acceptInvite(token)}
+ message="Melde dich an, um die Einladung anzunehmen."
+ />
+ ) : (
+ // Neuer User - Registrierung
+ acceptInvite(token, userId)}
+ />
+ )}
+
+ );
+}
+```
+
+### 9.3 Einladungs-Verwaltung (Admin)
+
+```tsx
+// pages/admin/Invitations.tsx
+
+function InvitationsAdmin() {
+ const { data: invitations } = useQuery({
+ queryKey: ['admin', 'invitations'],
+ queryFn: () => api.get('/admin/invitations').then(r => r.data)
+ });
+
+ const revokeInvite = useMutation({
+ mutationFn: (token: string) => api.delete(`/admin/invitations/${token}`),
+ onSuccess: () => queryClient.invalidateQueries(['admin', 'invitations'])
+ });
+
+ return (
+
+
Ausstehende Einladungen
+
+
+ Email
+ Mandant
+ Rolle
+ Erstellt
+ Gültig bis
+ Aktionen
+
+
+ {invitations?.map(inv => (
+
+ {inv.email}
+ {inv.mandateName}
+ {inv.roleLabel}
+ {formatDate(inv.createdAt)}
+ {formatDate(inv.expiresAt)}
+
+ revokeInvite.mutate(inv.token)}
+ title="Einladung widerrufen"
+ />
+
+
+ ))}
+
+
+
+ );
+}
+```
+
+---
+
+## 10. Zusammenfassung
+
+### 10.1 Wichtige Prinzipien
+
+| Prinzip | Umsetzung |
+|---------|-----------|
+| **Kein mandateId** | User gehört keinem Mandanten, arbeitet in Feature-Instanzen |
+| **Summarische Permissions** | Ein Request lädt alle Berechtigungen pro Instanz |
+| **Generisches Feature-Handling** | Alle Features haben gleiche Struktur |
+| **Instanz aus URL** | `/feature/{instanceId}/...` - Instanz-ID immer in URL |
+| **Navigation = Hierarchie** | Feature → Instanz (Subgruppe) → Objekte |
+| **System-Features immer verfügbar** | Profil, Settings, Logout ohne Instanz |
+
+### 10.2 State-Architektur
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Frontend State │
+├─────────────────────────────────────────────────────────┤
+│ │
+│ AuthStore │
+│ ├─ user: User │
+│ ├─ token: string │
+│ └─ isAuthenticated: boolean │
+│ │
+│ FeatureStore │
+│ ├─ features: Feature[] │
+│ │ └─ instances: FeatureInstance[] │
+│ │ └─ permissions: InstancePermissions │
+│ └─ getInstanceById(id): FeatureInstance │
+│ │
+│ Router (URL) │
+│ └─ /:featureCode/:instanceId/* → aktuelle Instanz │
+│ │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 10.3 API-Endpunkte (Frontend-relevant)
+
+| Endpunkt | Beschreibung |
+|----------|--------------|
+| `GET /features/my` | Alle Features + Instanzen + Permissions |
+| `GET /invite/validate?token=X` | Einladung validieren |
+| `POST /invite/accept` | Einladung annehmen |
+| `GET /admin/invitations` | Alle Einladungen (Admin) |
+| `DELETE /admin/invitations/{token}` | Einladung widerrufen |
+
+---
+
+## 11. Migration (Frontend)
+
+### 11.1 Zu entfernen
+
+- `currentMandateId` aus globalem State
+- `mandateId` aus API-Requests (Query-Params)
+- Mandanten-Switcher
+
+### 11.2 Zu ändern
+
+- **Navigation:** Feature → Instanz-Subgruppen → Objekte (hierarchisch)
+- **URLs:** Müssen `/:featureCode/:instanceId/...` enthalten
+- **Permission-Checks:** Auf `useTablePermission` + `useCurrentInstance` umstellen
+- **API-Calls:** Instanz-ID aus URL-Parameter verwenden
+
+### 11.3 Neu
+
+- `FeatureStore` mit allen Instanzen und Permissions
+- `useCurrentInstance()` Hook (liest aus URL)
+- `FeatureNavGroup` + `InstanceNavSubgroup` Komponenten
+- `PermissionGate` Wrapper
+- Einladungs-Flow UI