From 45eda1e4d49283a549e6ab81989ad56d2eeba831 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 4 Feb 2026 14:09:35 +0100
Subject: [PATCH] cleaned up all route and main references - no direct access
to db.getRecordset - only over interfaces
---
app.py | 7 -
modules/aicore/aicorePluginAnthropic.py | 4 +-
modules/aicore/aicorePluginInternal.py | 6 +-
modules/aicore/aicorePluginOpenai.py | 8 +-
modules/aicore/aicorePluginPerplexity.py | 4 +-
modules/aicore/aicorePluginTavily.py | 2 +-
modules/datamodels/datamodelAi.py | 4 +-
modules/datamodels/datamodelChat.py | 4 +-
modules/features/automation/mainAutomation.py | 140 ++++
modules/features/chatplayground/__init__.py | 6 +
.../interfaceFeatureChatplayground.py | 145 ++++
.../chatplayground/mainChatplayground.py | 273 +++++++
.../routeFeatureChatplayground.py | 233 ++++++
.../mainNeutralizePlayground.py | 12 +-
modules/features/realEstate/mainRealEstate.py | 56 +-
modules/features/trustee/mainTrustee.py | 69 +-
.../features/trustee/routeFeatureTrustee.py | 69 +-
modules/interfaces/interfaceAiObjects.py | 10 +-
modules/interfaces/interfaceBootstrap.py | 65 ++
modules/interfaces/interfaceDbApp.py | 771 ++++++++++++++++++
modules/routes/routeAdminFeatures.py | 215 ++---
modules/routes/routeAdminRbacExport.py | 129 ++-
modules/routes/routeAdminRbacRoles.py | 104 +--
modules/routes/routeAdminRbacRules.py | 43 +-
.../routes/routeAdminUserAccessOverview.py | 170 ++--
modules/routes/routeChat.py | 128 ---
modules/routes/routeDataConnections.py | 22 +-
modules/routes/routeDataMandates.py | 107 +--
modules/routes/routeDataUsers.py | 52 +-
modules/routes/routeDataWorkflows.py | 51 +-
modules/routes/routeGdpr.py | 118 +--
modules/routes/routeInvitations.py | 174 ++--
modules/routes/routeMessaging.py | 7 +-
modules/routes/routeNotifications.py | 177 ++--
modules/routes/routeSecurityAdmin.py | 12 +-
modules/routes/routeSecurityGoogle.py | 32 +-
modules/routes/routeSecurityLocal.py | 28 +-
modules/routes/routeSecurityMsft.py | 11 +-
modules/routes/routeSystem.py | 85 +-
modules/security/rbac.py | 26 +-
.../services/serviceChat/mainServiceChat.py | 2 +-
.../mainServiceExtraction.py | 22 +-
modules/shared/gdprDeletion.py | 18 +-
modules/system/mainSystem.py | 153 ++--
.../methods/methodAi/actions/process.py | 2 +-
tests/functional/test02_ai_models.py | 2 +-
46 files changed, 2462 insertions(+), 1316 deletions(-)
create mode 100644 modules/features/chatplayground/__init__.py
create mode 100644 modules/features/chatplayground/interfaceFeatureChatplayground.py
create mode 100644 modules/features/chatplayground/mainChatplayground.py
create mode 100644 modules/features/chatplayground/routeFeatureChatplayground.py
delete mode 100644 modules/routes/routeChat.py
diff --git a/app.py b/app.py
index ce3b3ff1..474de4d6 100644
--- a/app.py
+++ b/app.py
@@ -485,7 +485,6 @@ app.include_router(rbacAdminRulesRouter)
from modules.routes.routeMessaging import router as messagingRouter
app.include_router(messagingRouter)
-# Phase 8: New Feature Routes
from modules.routes.routeAdminFeatures import router as featuresAdminRouter
app.include_router(featuresAdminRouter)
@@ -504,12 +503,6 @@ app.include_router(userAccessOverviewRouter)
from modules.routes.routeGdpr import router as gdprRouter
app.include_router(gdprRouter)
-from modules.routes.routeChat import router as chatRouter
-app.include_router(chatRouter)
-
-from modules.features.chatbot.routeFeatureChatbot import router as chatbotFeatureRouter
-app.include_router(chatbotFeatureRouter)
-
# ============================================================================
# SYSTEM ROUTES (Navigation, etc.)
# ============================================================================
diff --git a/modules/aicore/aicorePluginAnthropic.py b/modules/aicore/aicorePluginAnthropic.py
index 0d80aeaa..eeea9a07 100644
--- a/modules/aicore/aicorePluginAnthropic.py
+++ b/modules/aicore/aicorePluginAnthropic.py
@@ -72,7 +72,7 @@ class AiAnthropic(BaseConnectorAi):
(OperationTypeEnum.DATA_EXTRACT, 8)
),
version="claude-sonnet-4-5-20250929",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
),
AiModel(
name="claude-sonnet-4-5-20250929",
@@ -93,7 +93,7 @@ class AiAnthropic(BaseConnectorAi):
(OperationTypeEnum.IMAGE_ANALYSE, 10)
),
version="claude-sonnet-4-5-20250929",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.015 + (bytesReceived / 4 / 1000) * 0.075
)
]
diff --git a/modules/aicore/aicorePluginInternal.py b/modules/aicore/aicorePluginInternal.py
index 1b73c27e..59854629 100644
--- a/modules/aicore/aicorePluginInternal.py
+++ b/modules/aicore/aicorePluginInternal.py
@@ -40,7 +40,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(),
version="internal-extractor-v1",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.001 + (bytesSent + bytesReceived) / (1024 * 1024) * 0.01
),
AiModel(
name="internal-generator",
@@ -60,7 +60,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.BASIC,
operationTypes=createOperationTypeRatings(),
version="internal-generator-v1",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.002 + (bytesReceived / (1024 * 1024)) * 0.005
),
AiModel(
name="internal-renderer",
@@ -80,7 +80,7 @@ class AiInternal(BaseConnectorAi):
processingMode=ProcessingModeEnum.DETAILED,
operationTypes=createOperationTypeRatings(),
version="internal-renderer-v1",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.003 + (bytesReceived / (1024 * 1024)) * 0.008
)
]
diff --git a/modules/aicore/aicorePluginOpenai.py b/modules/aicore/aicorePluginOpenai.py
index c35c6dd6..931ece10 100644
--- a/modules/aicore/aicorePluginOpenai.py
+++ b/modules/aicore/aicorePluginOpenai.py
@@ -72,7 +72,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.DATA_EXTRACT, 7)
),
version="gpt-4o",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
),
AiModel(
name="gpt-3.5-turbo",
@@ -97,7 +97,7 @@ class AiOpenai(BaseConnectorAi):
# Note: GPT-3.5-turbo does NOT support vision/image operations
),
version="gpt-3.5-turbo",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.0015 + (bytesReceived / 4 / 1000) * 0.002
),
AiModel(
name="gpt-4o",
@@ -118,7 +118,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.IMAGE_ANALYSE, 9)
),
version="gpt-4o",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.03 + (bytesReceived / 4 / 1000) * 0.06
),
AiModel(
name="dall-e-3",
@@ -140,7 +140,7 @@ class AiOpenai(BaseConnectorAi):
(OperationTypeEnum.IMAGE_GENERATE, 10)
),
version="dall-e-3",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.04
)
]
diff --git a/modules/aicore/aicorePluginPerplexity.py b/modules/aicore/aicorePluginPerplexity.py
index e6d1ba10..7cb5e928 100644
--- a/modules/aicore/aicorePluginPerplexity.py
+++ b/modules/aicore/aicorePluginPerplexity.py
@@ -74,7 +74,7 @@ class AiPerplexity(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 7)
),
version="sonar",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005
),
AiModel(
name="sonar-pro",
@@ -97,7 +97,7 @@ class AiPerplexity(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 8)
),
version="sonar-pro",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01
)
]
diff --git a/modules/aicore/aicorePluginTavily.py b/modules/aicore/aicorePluginTavily.py
index 1d2ece75..635cd4eb 100644
--- a/modules/aicore/aicorePluginTavily.py
+++ b/modules/aicore/aicorePluginTavily.py
@@ -71,7 +71,7 @@ class AiTavily(BaseConnectorAi):
(OperationTypeEnum.WEB_CRAWL, 10)
),
version="tavily-search",
- calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: 0.008 # Simple flat rate
+ calculatepriceCHF=lambda processingTime, bytesSent, bytesReceived: 0.008 # Simple flat rate
)
]
diff --git a/modules/datamodels/datamodelAi.py b/modules/datamodels/datamodelAi.py
index 69d51871..c9d81bfa 100644
--- a/modules/datamodels/datamodelAi.py
+++ b/modules/datamodels/datamodelAi.py
@@ -98,7 +98,7 @@ class AiModel(BaseModel):
# Function reference (not serialized)
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
- calculatePriceUsd: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
+ calculatepriceCHF: Optional[Callable] = Field(default=None, exclude=True, description="Function to calculate price in USD")
# Selection criteria - capabilities with ratings
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.")
@@ -159,7 +159,7 @@ class AiCallResponse(BaseModel):
content: str = Field(description="AI response content")
modelName: str = Field(description="Selected model name")
- priceUsd: float = Field(default=0.0, description="Calculated price in USD")
+ priceCHF: float = Field(default=0.0, description="Calculated price in USD")
processingTime: float = Field(default=0.0, description="Duration in seconds")
bytesSent: int = Field(default=0, description="Input data size in bytes")
bytesReceived: int = Field(default=0, description="Output data size in bytes")
diff --git a/modules/datamodels/datamodelChat.py b/modules/datamodels/datamodelChat.py
index 22c07aa2..8ba3ced1 100644
--- a/modules/datamodels/datamodelChat.py
+++ b/modules/datamodels/datamodelChat.py
@@ -26,7 +26,7 @@ class ChatStat(BaseModel):
errorCount: Optional[int] = Field(None, description="Number of errors encountered")
process: Optional[str] = Field(None, description="The process that delivers the stats data (e.g. 'action.outlook.readMails', 'ai.process.document.name')")
engine: Optional[str] = Field(None, description="The engine used (e.g. 'ai.anthropic.35', 'ai.tavily.basic', 'renderer.docx')")
- priceUsd: Optional[float] = Field(None, description="Calculated price in USD for the operation")
+ priceCHF: Optional[float] = Field(None, description="Calculated price in USD for the operation")
registerModelLabels(
@@ -41,7 +41,7 @@ registerModelLabels(
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"},
"process": {"en": "Process", "fr": "Processus"},
"engine": {"en": "Engine", "fr": "Moteur"},
- "priceUsd": {"en": "Price USD", "fr": "Prix USD"},
+ "priceCHF": {"en": "Price USD", "fr": "Prix USD"},
},
)
diff --git a/modules/features/automation/mainAutomation.py b/modules/features/automation/mainAutomation.py
index 88828442..2b8443a9 100644
--- a/modules/features/automation/mainAutomation.py
+++ b/modules/features/automation/mainAutomation.py
@@ -59,7 +59,24 @@ RESOURCE_OBJECTS = [
]
# Template roles for this feature
+# IMPORTANT: "viewer" role is required for automatic user assignment!
TEMPLATE_ROLES = [
+ {
+ "roleLabel": "viewer",
+ "description": {
+ "en": "Automation Viewer - View automations and execution results",
+ "de": "Automatisierungs-Betrachter - Automatisierungen und Ausführungsergebnisse einsehen",
+ "fr": "Visualiseur automatisation - Consulter les automatisations et résultats"
+ },
+ "accessRules": [
+ # UI access to all views
+ {"context": "UI", "item": "ui.feature.automation.definitions", "view": True},
+ {"context": "UI", "item": "ui.feature.automation.templates", "view": True},
+ {"context": "UI", "item": "ui.feature.automation.logs", "view": True},
+ # Read-only DATA access
+ {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
+ ]
+ },
{
"roleLabel": "automation-admin",
"description": {
@@ -161,9 +178,132 @@ def registerFeature(catalogService) -> bool:
meta=resObj.get("meta")
)
+ # Sync template roles to database
+ _syncTemplateRolesToDb()
+
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
+
+
+def _syncTemplateRolesToDb() -> int:
+ """
+ Sync template roles and their AccessRules to the database.
+ Creates global template roles (mandateId=None) if they don't exist.
+
+ Returns:
+ Number of roles created/updated
+ """
+ try:
+ from modules.interfaces.interfaceDbApp import getRootInterface
+ from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+
+ rootInterface = getRootInterface()
+
+ # Get existing template roles for this feature (Pydantic models)
+ existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
+ # Filter to template roles (mandateId is None)
+ templateRoles = [r for r in existingRoles if r.mandateId is None]
+ existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
+
+ createdCount = 0
+ for roleTemplate in TEMPLATE_ROLES:
+ roleLabel = roleTemplate["roleLabel"]
+
+ if roleLabel in existingRoleLabels:
+ roleId = existingRoleLabels[roleLabel]
+ logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
+
+ # Ensure AccessRules exist for this role
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
+ else:
+ # Create new template role
+ newRole = Role(
+ roleLabel=roleLabel,
+ description=roleTemplate.get("description", {}),
+ featureCode=FEATURE_CODE,
+ mandateId=None, # Global template
+ featureInstanceId=None,
+ isSystemRole=False
+ )
+ createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
+ roleId = createdRole.get("id")
+
+ # Create AccessRules for this role
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
+
+ logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
+ createdCount += 1
+
+ if createdCount > 0:
+ logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
+
+ return createdCount
+
+ except Exception as e:
+ logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
+ return 0
+
+
+def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
+ """
+ Ensure AccessRules exist for a role based on templates.
+
+ Args:
+ rootInterface: Root interface instance
+ roleId: Role ID
+ ruleTemplates: List of rule templates
+
+ Returns:
+ Number of rules created
+ """
+ from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
+
+ # Get existing rules for this role (Pydantic models)
+ existingRules = rootInterface.getAccessRulesByRole(roleId)
+
+ # Create a set of existing rule signatures to avoid duplicates
+ existingSignatures = set()
+ for rule in existingRules:
+ sig = (str(rule.context) if rule.context else None, rule.item)
+ existingSignatures.add(sig)
+
+ createdCount = 0
+ for template in ruleTemplates:
+ context = template.get("context", "UI")
+ item = template.get("item")
+ sig = (context, item)
+
+ if sig in existingSignatures:
+ continue
+
+ # Map context string to enum
+ if context == "UI":
+ contextEnum = AccessRuleContext.UI
+ elif context == "DATA":
+ contextEnum = AccessRuleContext.DATA
+ elif context == "RESOURCE":
+ contextEnum = AccessRuleContext.RESOURCE
+ else:
+ contextEnum = context
+
+ newRule = AccessRule(
+ roleId=roleId,
+ context=contextEnum,
+ item=item,
+ view=template.get("view", False),
+ read=template.get("read"),
+ create=template.get("create"),
+ update=template.get("update"),
+ delete=template.get("delete"),
+ )
+ rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
+ createdCount += 1
+
+ if createdCount > 0:
+ logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
+
+ return createdCount
diff --git a/modules/features/chatplayground/__init__.py b/modules/features/chatplayground/__init__.py
new file mode 100644
index 00000000..4b2f2bd4
--- /dev/null
+++ b/modules/features/chatplayground/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) 2025 Patrick Motsch
+# All rights reserved.
+"""
+Chat Playground Feature Container.
+Provides workflow-based chat playground functionality.
+"""
diff --git a/modules/features/chatplayground/interfaceFeatureChatplayground.py b/modules/features/chatplayground/interfaceFeatureChatplayground.py
new file mode 100644
index 00000000..5a2548ba
--- /dev/null
+++ b/modules/features/chatplayground/interfaceFeatureChatplayground.py
@@ -0,0 +1,145 @@
+# Copyright (c) 2025 Patrick Motsch
+# All rights reserved.
+"""
+Chat Playground Feature Interface.
+Wrapper around interfaceDbChat with feature instance context.
+"""
+
+import logging
+from typing import Dict, Any, List, Optional
+
+from modules.datamodels.datamodelUam import User
+from modules.interfaces import interfaceDbChat
+
+logger = logging.getLogger(__name__)
+
+# Feature code constant
+FEATURE_CODE = "chatplayground"
+
+# Singleton instances cache
+_instances: Dict[str, "ChatPlaygroundObjects"] = {}
+
+
+def getInterface(currentUser: User, mandateId: str = None, featureInstanceId: str = None) -> "ChatPlaygroundObjects":
+ """
+ Factory function to get or create a ChatPlaygroundObjects instance.
+ Uses singleton pattern per user context.
+
+ Args:
+ currentUser: Current user object
+ mandateId: Mandate ID
+ featureInstanceId: Feature instance ID
+
+ Returns:
+ ChatPlaygroundObjects instance
+ """
+ cacheKey = f"{currentUser.id}_{mandateId}_{featureInstanceId}"
+
+ if cacheKey not in _instances:
+ _instances[cacheKey] = ChatPlaygroundObjects(currentUser, mandateId, featureInstanceId)
+ else:
+ # Update context if needed
+ _instances[cacheKey].setUserContext(currentUser, mandateId, featureInstanceId)
+
+ return _instances[cacheKey]
+
+
+class ChatPlaygroundObjects:
+ """
+ Chat Playground feature interface.
+ Wraps the shared interfaceDbChat with feature instance context.
+ """
+
+ FEATURE_CODE = FEATURE_CODE
+
+ def __init__(self, currentUser: User, mandateId: str = None, featureInstanceId: str = None):
+ """
+ Initialize the Chat Playground interface.
+
+ Args:
+ currentUser: Current user object
+ mandateId: Mandate ID
+ featureInstanceId: Feature instance ID
+ """
+ self.currentUser = currentUser
+ self.mandateId = mandateId
+ self.featureInstanceId = featureInstanceId
+
+ # Get the underlying chat interface
+ self._chatInterface = interfaceDbChat.getInterface(
+ currentUser,
+ mandateId=mandateId,
+ featureInstanceId=featureInstanceId
+ )
+
+ def setUserContext(self, currentUser: User, mandateId: str = None, featureInstanceId: str = None):
+ """
+ Update the user context.
+
+ Args:
+ currentUser: Current user object
+ mandateId: Mandate ID
+ featureInstanceId: Feature instance ID
+ """
+ self.currentUser = currentUser
+ self.mandateId = mandateId
+ self.featureInstanceId = featureInstanceId
+
+ # Update underlying interface
+ self._chatInterface = interfaceDbChat.getInterface(
+ currentUser,
+ mandateId=mandateId,
+ featureInstanceId=featureInstanceId
+ )
+
+ # =========================================================================
+ # Delegated methods from interfaceDbChat
+ # =========================================================================
+
+ def getWorkflow(self, workflowId: str) -> Optional[Dict[str, Any]]:
+ """Get a workflow by ID."""
+ return self._chatInterface.getWorkflow(workflowId)
+
+ def getWorkflows(self, pagination=None) -> Dict[str, Any]:
+ """Get all workflows with pagination."""
+ return self._chatInterface.getWorkflows(pagination=pagination)
+
+ def getUnifiedChatData(self, workflowId: str, afterTimestamp: float = None) -> Dict[str, Any]:
+ """Get unified chat data for a workflow."""
+ return self._chatInterface.getUnifiedChatData(workflowId, afterTimestamp)
+
+ def createWorkflow(self, workflow) -> Dict[str, Any]:
+ """Create a new workflow."""
+ return self._chatInterface.createWorkflow(workflow)
+
+ def updateWorkflow(self, workflowId: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+ """Update a workflow."""
+ return self._chatInterface.updateWorkflow(workflowId, updates)
+
+ def deleteWorkflow(self, workflowId: str) -> bool:
+ """Delete a workflow."""
+ return self._chatInterface.deleteWorkflow(workflowId)
+
+ def getMessages(self, workflowId: str) -> List[Dict[str, Any]]:
+ """Get messages for a workflow."""
+ return self._chatInterface.getMessages(workflowId)
+
+ def createMessage(self, message) -> Dict[str, Any]:
+ """Create a new message."""
+ return self._chatInterface.createMessage(message)
+
+ def getLogs(self, workflowId: str) -> List[Dict[str, Any]]:
+ """Get logs for a workflow."""
+ return self._chatInterface.getLogs(workflowId)
+
+ def createLog(self, log) -> Dict[str, Any]:
+ """Create a new log entry."""
+ return self._chatInterface.createLog(log)
+
+ def getStats(self, workflowId: str) -> List[Dict[str, Any]]:
+ """Get stats for a workflow."""
+ return self._chatInterface.getStats(workflowId)
+
+ def createStat(self, stat) -> Dict[str, Any]:
+ """Create a new stat entry."""
+ return self._chatInterface.createStat(stat)
diff --git a/modules/features/chatplayground/mainChatplayground.py b/modules/features/chatplayground/mainChatplayground.py
new file mode 100644
index 00000000..ed0e2868
--- /dev/null
+++ b/modules/features/chatplayground/mainChatplayground.py
@@ -0,0 +1,273 @@
+# Copyright (c) 2025 Patrick Motsch
+# All rights reserved.
+"""
+Chat Playground Feature Container - Main Module.
+Handles feature initialization and RBAC catalog registration.
+"""
+
+import logging
+from typing import Dict, List, Any
+
+logger = logging.getLogger(__name__)
+
+# Feature metadata
+FEATURE_CODE = "chatplayground"
+FEATURE_LABEL = {"en": "Chat Playground", "de": "Chat Playground", "fr": "Chat Playground"}
+FEATURE_ICON = "mdi-message-text"
+
+# UI Objects for RBAC catalog
+UI_OBJECTS = [
+ {
+ "objectKey": "ui.feature.chatplayground.playground",
+ "label": {"en": "Playground", "de": "Playground", "fr": "Playground"},
+ "meta": {"area": "playground"}
+ },
+ {
+ "objectKey": "ui.feature.chatplayground.workflows",
+ "label": {"en": "Workflows", "de": "Workflows", "fr": "Workflows"},
+ "meta": {"area": "workflows"}
+ },
+]
+
+# Resource Objects for RBAC catalog
+RESOURCE_OBJECTS = [
+ {
+ "objectKey": "resource.feature.chatplayground.start",
+ "label": {"en": "Start Workflow", "de": "Workflow starten", "fr": "Démarrer workflow"},
+ "meta": {"endpoint": "/api/chatplayground/{instanceId}/start", "method": "POST"}
+ },
+ {
+ "objectKey": "resource.feature.chatplayground.stop",
+ "label": {"en": "Stop Workflow", "de": "Workflow stoppen", "fr": "Arrêter workflow"},
+ "meta": {"endpoint": "/api/chatplayground/{instanceId}/{workflowId}/stop", "method": "POST"}
+ },
+ {
+ "objectKey": "resource.feature.chatplayground.chatData",
+ "label": {"en": "Get Chat Data", "de": "Chat-Daten abrufen", "fr": "Récupérer données chat"},
+ "meta": {"endpoint": "/api/chatplayground/{instanceId}/{workflowId}/chatData", "method": "GET"}
+ },
+]
+
+# Template roles for this feature
+# IMPORTANT: "viewer" role is required for automatic user assignment!
+TEMPLATE_ROLES = [
+ {
+ "roleLabel": "viewer",
+ "description": {
+ "en": "Chat Playground Viewer - View and use chat playground",
+ "de": "Chat Playground Betrachter - Chat Playground ansehen und nutzen",
+ "fr": "Visualiseur Chat Playground - Consulter et utiliser le chat playground"
+ },
+ "accessRules": [
+ # UI access to all views
+ {"context": "UI", "item": "ui.feature.chatplayground.playground", "view": True},
+ {"context": "UI", "item": "ui.feature.chatplayground.workflows", "view": True},
+ # Resource access
+ {"context": "RESOURCE", "item": "resource.feature.chatplayground.start", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.chatplayground.stop", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.chatplayground.chatData", "view": True},
+ # DATA access (own records)
+ {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"},
+ ]
+ },
+ {
+ "roleLabel": "admin",
+ "description": {
+ "en": "Chat Playground Admin - Full access to chat playground",
+ "de": "Chat Playground Admin - Vollzugriff auf Chat Playground",
+ "fr": "Administrateur Chat Playground - Accès complet au chat playground"
+ },
+ "accessRules": [
+ # Full UI access
+ {"context": "UI", "item": None, "view": True},
+ # Full resource access
+ {"context": "RESOURCE", "item": None, "view": True},
+ # Full DATA access
+ {"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
+ ]
+ },
+]
+
+
+def getFeatureDefinition() -> Dict[str, Any]:
+ """Return the feature definition for registration."""
+ return {
+ "code": FEATURE_CODE,
+ "label": FEATURE_LABEL,
+ "icon": FEATURE_ICON
+ }
+
+
+def getUiObjects() -> List[Dict[str, Any]]:
+ """Return UI objects for RBAC catalog registration."""
+ return UI_OBJECTS
+
+
+def getResourceObjects() -> List[Dict[str, Any]]:
+ """Return resource objects for RBAC catalog registration."""
+ return RESOURCE_OBJECTS
+
+
+def getTemplateRoles() -> List[Dict[str, Any]]:
+ """Return template roles for this feature."""
+ return TEMPLATE_ROLES
+
+
+def registerFeature(catalogService) -> bool:
+ """
+ Register this feature's RBAC objects in the catalog.
+
+ Args:
+ catalogService: The RBAC catalog service instance
+
+ Returns:
+ True if registration was successful
+ """
+ try:
+ # Register UI objects
+ for uiObj in UI_OBJECTS:
+ catalogService.registerUiObject(
+ featureCode=FEATURE_CODE,
+ objectKey=uiObj["objectKey"],
+ label=uiObj["label"],
+ meta=uiObj.get("meta")
+ )
+
+ # Register Resource objects
+ for resObj in RESOURCE_OBJECTS:
+ catalogService.registerResourceObject(
+ featureCode=FEATURE_CODE,
+ objectKey=resObj["objectKey"],
+ label=resObj["label"],
+ meta=resObj.get("meta")
+ )
+
+ # Sync template roles to database
+ _syncTemplateRolesToDb()
+
+ logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
+ return True
+
+ except Exception as e:
+ logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
+ return False
+
+
+def _syncTemplateRolesToDb() -> int:
+ """
+ Sync template roles and their AccessRules to the database.
+ Creates global template roles (mandateId=None) if they don't exist.
+
+ Returns:
+ Number of roles created/updated
+ """
+ try:
+ from modules.interfaces.interfaceDbApp import getRootInterface
+ from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+
+ rootInterface = getRootInterface()
+
+ # Get existing template roles for this feature (Pydantic models)
+ existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
+ # Filter to template roles (mandateId is None)
+ templateRoles = [r for r in existingRoles if r.mandateId is None]
+ existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
+
+ createdCount = 0
+ for roleTemplate in TEMPLATE_ROLES:
+ roleLabel = roleTemplate["roleLabel"]
+
+ if roleLabel in existingRoleLabels:
+ roleId = existingRoleLabels[roleLabel]
+ logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
+
+ # Ensure AccessRules exist for this role
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
+ else:
+ # Create new template role
+ newRole = Role(
+ roleLabel=roleLabel,
+ description=roleTemplate.get("description", {}),
+ featureCode=FEATURE_CODE,
+ mandateId=None, # Global template
+ featureInstanceId=None,
+ isSystemRole=False
+ )
+ createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
+ roleId = createdRole.get("id")
+
+ # Create AccessRules for this role
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
+
+ logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
+ createdCount += 1
+
+ if createdCount > 0:
+ logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
+
+ return createdCount
+
+ except Exception as e:
+ logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
+ return 0
+
+
+def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
+ """
+ Ensure AccessRules exist for a role based on templates.
+
+ Args:
+ rootInterface: Root interface instance
+ roleId: Role ID
+ ruleTemplates: List of rule templates
+
+ Returns:
+ Number of rules created
+ """
+ from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
+
+ # Get existing rules for this role (Pydantic models)
+ existingRules = rootInterface.getAccessRulesByRole(roleId)
+
+ # Create a set of existing rule signatures to avoid duplicates
+ existingSignatures = set()
+ for rule in existingRules:
+ sig = (str(rule.context) if rule.context else None, rule.item)
+ existingSignatures.add(sig)
+
+ createdCount = 0
+ for template in ruleTemplates:
+ context = template.get("context", "UI")
+ item = template.get("item")
+ sig = (context, item)
+
+ if sig in existingSignatures:
+ continue
+
+ # Map context string to enum
+ if context == "UI":
+ contextEnum = AccessRuleContext.UI
+ elif context == "DATA":
+ contextEnum = AccessRuleContext.DATA
+ elif context == "RESOURCE":
+ contextEnum = AccessRuleContext.RESOURCE
+ else:
+ contextEnum = context
+
+ newRule = AccessRule(
+ roleId=roleId,
+ context=contextEnum,
+ item=item,
+ view=template.get("view", False),
+ read=template.get("read"),
+ create=template.get("create"),
+ update=template.get("update"),
+ delete=template.get("delete"),
+ )
+ rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
+ createdCount += 1
+
+ if createdCount > 0:
+ logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
+
+ return createdCount
diff --git a/modules/features/chatplayground/routeFeatureChatplayground.py b/modules/features/chatplayground/routeFeatureChatplayground.py
new file mode 100644
index 00000000..6a76e70e
--- /dev/null
+++ b/modules/features/chatplayground/routeFeatureChatplayground.py
@@ -0,0 +1,233 @@
+# Copyright (c) 2025 Patrick Motsch
+# All rights reserved.
+"""
+Chat Playground Feature Routes.
+Implements the endpoints for chat playground workflow management as a feature.
+"""
+
+import logging
+from typing import Optional, Dict, Any
+from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request
+
+# Import auth modules
+from modules.auth import limiter, getRequestContext, RequestContext
+
+# Import interfaces
+from modules.interfaces import interfaceDbChat
+
+# Import models
+from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
+
+# Import workflow control functions
+from modules.workflows.automation import chatStart, chatStop
+
+# Configure logger
+logger = logging.getLogger(__name__)
+
+# Create router for chat playground feature endpoints
+router = APIRouter(
+ prefix="/api/chatplayground",
+ tags=["Chat Playground Feature"],
+ responses={404: {"description": "Not found"}}
+)
+
+
+def _getServiceChat(context: RequestContext, featureInstanceId: str = None):
+ """Get chat interface with feature instance context."""
+ return interfaceDbChat.getInterface(
+ context.user,
+ mandateId=str(context.mandateId) if context.mandateId else None,
+ featureInstanceId=featureInstanceId
+ )
+
+
+async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
+ """
+ Validate that user has access to the feature instance.
+
+ Args:
+ instanceId: Feature instance ID
+ context: Request context
+
+ Returns:
+ mandateId for the instance
+
+ Raises:
+ HTTPException if access is denied
+ """
+ from modules.interfaces.interfaceDbApp import getRootInterface
+
+ rootInterface = getRootInterface()
+
+ # Get feature instance (Pydantic model)
+ instance = rootInterface.getFeatureInstance(instanceId)
+ if not instance:
+ raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
+
+ # Check user has access to this instance using interface method
+ featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
+
+ if not featureAccess or not featureAccess.enabled:
+ raise HTTPException(status_code=403, detail="Access denied to this feature instance")
+
+ return str(instance.mandateId) if instance.mandateId else None
+
+
+# Workflow start endpoint
+@router.post("/{instanceId}/start", response_model=ChatWorkflow)
+@limiter.limit("120/minute")
+async def start_workflow(
+ request: Request,
+ instanceId: str = Path(..., description="Feature instance ID"),
+ workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
+ workflowMode: WorkflowModeEnum = Query(..., description="Workflow mode: 'Dynamic' or 'Automation' (mandatory)"),
+ userInput: UserInputRequest = Body(...),
+ context: RequestContext = Depends(getRequestContext)
+) -> ChatWorkflow:
+ """
+ Starts a new workflow or continues an existing one.
+
+ Args:
+ instanceId: Feature instance ID
+ workflowMode: "Dynamic" for iterative dynamic-style processing, "Automation" for automated workflow execution
+ """
+ try:
+ # Validate access and get mandate ID
+ mandateId = await _validateInstanceAccess(instanceId, context)
+
+ # Start or continue workflow
+ workflow = await chatStart(
+ context.user,
+ userInput,
+ workflowMode,
+ workflowId,
+ mandateId=mandateId,
+ featureInstanceId=instanceId
+ )
+
+ return workflow
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error in start_workflow: {str(e)}")
+ raise HTTPException(
+ status_code=500,
+ detail=str(e)
+ )
+
+
+# Stop workflow endpoint
+@router.post("/{instanceId}/{workflowId}/stop", response_model=ChatWorkflow)
+@limiter.limit("120/minute")
+async def stop_workflow(
+ request: Request,
+ instanceId: str = Path(..., description="Feature instance ID"),
+ workflowId: str = Path(..., description="ID of the workflow to stop"),
+ context: RequestContext = Depends(getRequestContext)
+) -> ChatWorkflow:
+ """Stops a running workflow."""
+ try:
+ # Validate access and get mandate ID
+ mandateId = await _validateInstanceAccess(instanceId, context)
+
+ # Stop workflow
+ workflow = await chatStop(
+ context.user,
+ workflowId,
+ mandateId=mandateId,
+ featureInstanceId=instanceId
+ )
+
+ return workflow
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error in stop_workflow: {str(e)}")
+ raise HTTPException(
+ status_code=500,
+ detail=str(e)
+ )
+
+
+# Unified Chat Data Endpoint for Polling
+@router.get("/{instanceId}/{workflowId}/chatData")
+@limiter.limit("120/minute")
+async def get_workflow_chat_data(
+ request: Request,
+ instanceId: str = Path(..., description="Feature instance ID"),
+ workflowId: str = Path(..., description="ID of the workflow"),
+ afterTimestamp: Optional[float] = Query(None, description="Unix timestamp to get data after"),
+ context: RequestContext = Depends(getRequestContext)
+) -> Dict[str, Any]:
+ """
+ Get unified chat data (messages, logs, stats) for a workflow with timestamp-based selective data transfer.
+ Returns all data types in chronological order based on _createdAt timestamp.
+ """
+ try:
+ # Validate access
+ await _validateInstanceAccess(instanceId, context)
+
+ # Get service with feature instance context
+ chatInterface = _getServiceChat(context, featureInstanceId=instanceId)
+
+ # Verify workflow exists
+ workflow = chatInterface.getWorkflow(workflowId)
+ if not workflow:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Workflow with ID {workflowId} not found"
+ )
+
+ # Get unified chat data
+ chatData = chatInterface.getUnifiedChatData(workflowId, afterTimestamp)
+
+ return chatData
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error getting unified chat data: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error getting unified chat data: {str(e)}"
+ )
+
+
+# Get workflows for this instance
+@router.get("/{instanceId}/workflows")
+@limiter.limit("120/minute")
+async def get_workflows(
+ request: Request,
+ instanceId: str = Path(..., description="Feature instance ID"),
+ page: int = Query(1, ge=1, description="Page number"),
+ pageSize: int = Query(20, ge=1, le=100, description="Items per page"),
+ context: RequestContext = Depends(getRequestContext)
+) -> Dict[str, Any]:
+ """
+ Get all workflows for this feature instance.
+ """
+ try:
+ # Validate access
+ await _validateInstanceAccess(instanceId, context)
+
+ # Get service with feature instance context
+ chatInterface = _getServiceChat(context, featureInstanceId=instanceId)
+
+ # Get workflows with pagination
+ from modules.datamodels.datamodelPagination import PaginationParams
+ pagination = PaginationParams(page=page, pageSize=pageSize)
+
+ result = chatInterface.getWorkflows(pagination=pagination)
+
+ return result
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error getting workflows: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error getting workflows: {str(e)}"
+ )
diff --git a/modules/features/neutralization/mainNeutralizePlayground.py b/modules/features/neutralization/mainNeutralizePlayground.py
index bf9aa087..159faf04 100644
--- a/modules/features/neutralization/mainNeutralizePlayground.py
+++ b/modules/features/neutralization/mainNeutralizePlayground.py
@@ -182,20 +182,18 @@ class SharepointProcessor:
async def _getSharepointConnection(self, sharepointPath: str = None):
try:
- connections = self.services.interfaceDbApp.db.getRecordset(
- UserConnection,
- recordFilter={"userId": self.services.interfaceDbApp.userId}
- )
- msftConnections = [c for c in connections if c.get('authority') == 'msft']
+ # Use interface method to get user connections
+ connections = self.services.interfaceDbApp.getUserConnections(self.services.interfaceDbApp.userId)
+ msftConnections = [c for c in connections if c.authority == 'msft']
if not msftConnections:
logger.warning('No Microsoft connections found for user')
return None
if len(msftConnections) == 1:
- logger.info(f"Found single Microsoft connection: {msftConnections[0].get('id')}")
+ logger.info(f"Found single Microsoft connection: {msftConnections[0].id}")
return msftConnections[0]
if sharepointPath:
return await self._matchConnectionToPath(msftConnections, sharepointPath)
- logger.info(f"Multiple Microsoft connections found, using first one: {msftConnections[0].get('id')}")
+ logger.info(f"Multiple Microsoft connections found, using first one: {msftConnections[0].id}")
return msftConnections[0]
except Exception:
logger.error('Error getting SharePoint connection')
diff --git a/modules/features/realEstate/mainRealEstate.py b/modules/features/realEstate/mainRealEstate.py
index 5e43ceab..8562f5b8 100644
--- a/modules/features/realEstate/mainRealEstate.py
+++ b/modules/features/realEstate/mainRealEstate.py
@@ -165,13 +165,11 @@ def _syncTemplateRolesToDb() -> int:
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
- db = rootInterface.db
- existingRoles = db.getRecordset(
- Role,
- recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
- )
- existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
+ # Get existing template roles (Pydantic models)
+ existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
+ templateRoles = [r for r in existingRoles if r.mandateId is None]
+ existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
@@ -179,7 +177,7 @@ def _syncTemplateRolesToDb() -> int:
if roleLabel in existingRoleLabels:
roleId = existingRoleLabels[roleLabel]
- _ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
newRole = Role(
roleLabel=roleLabel,
@@ -189,65 +187,65 @@ def _syncTemplateRolesToDb() -> int:
featureInstanceId=None,
isSystemRole=False
)
- createdRole = db.recordCreate(Role, newRole.model_dump())
+ createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
existingRoleLabels[roleLabel] = roleId
- _ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logging.getLogger(__name__).info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
if createdCount > 0:
logging.getLogger(__name__).info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
- _repairInstanceRolesAccessRules(db, existingRoleLabels)
+ _repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
return createdCount
except Exception as e:
logging.getLogger(__name__).error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
return 0
-def _repairInstanceRolesAccessRules(db, templateRoleLabels: dict) -> int:
+def _repairInstanceRolesAccessRules(rootInterface, templateRoleLabels: dict) -> int:
"""Repair instance-specific roles by copying AccessRules from their template roles."""
from modules.datamodels.datamodelRbac import Role, AccessRule
repairedCount = 0
- allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
- instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
+ allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
+ instanceRoles = [r for r in allRoles if r.mandateId is not None]
for instanceRole in instanceRoles:
- roleLabel = instanceRole.get("roleLabel")
- instanceRoleId = instanceRole.get("id")
+ roleLabel = instanceRole.roleLabel
+ instanceRoleId = str(instanceRole.id)
templateRoleId = templateRoleLabels.get(roleLabel)
if not templateRoleId:
continue
- existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
+ existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
if existingRules:
continue
- templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
+ templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
if not templateRules:
continue
for rule in templateRules:
newRule = AccessRule(
roleId=instanceRoleId,
- context=rule.get("context"),
- item=rule.get("item"),
- view=rule.get("view", False),
- read=rule.get("read"),
- create=rule.get("create"),
- update=rule.get("update"),
- delete=rule.get("delete"),
+ context=rule.context,
+ item=rule.item,
+ view=rule.view if rule.view else False,
+ read=rule.read,
+ create=rule.create,
+ update=rule.update,
+ delete=rule.delete,
)
- db.recordCreate(AccessRule, newRule.model_dump())
+ rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
repairedCount += 1
return repairedCount
-def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: list) -> int:
+def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: list) -> int:
"""Ensure AccessRules exist for a role based on templates."""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
- existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
- existingSignatures = {(r.get("context"), r.get("item")) for r in existingRules}
+ existingRules = rootInterface.getAccessRulesByRole(roleId)
+ existingSignatures = {(str(r.context) if r.context else None, r.item) for r in existingRules}
createdCount = 0
for template in ruleTemplates or []:
@@ -273,7 +271,7 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: list) -> int:
update=template.get("update"),
delete=template.get("delete"),
)
- db.recordCreate(AccessRule, newRule.model_dump())
+ rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
existingSignatures.add((context, item))
return createdCount
diff --git a/modules/features/trustee/mainTrustee.py b/modules/features/trustee/mainTrustee.py
index 4f1694b5..ad449d8f 100644
--- a/modules/features/trustee/mainTrustee.py
+++ b/modules/features/trustee/mainTrustee.py
@@ -267,14 +267,11 @@ def _syncTemplateRolesToDb() -> int:
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
- db = rootInterface.db
- # Get existing template roles for this feature
- existingRoles = db.getRecordset(
- Role,
- recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
- )
- existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
+ # Get existing template roles for this feature (Pydantic models)
+ existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
+ templateRoles = [r for r in existingRoles if r.mandateId is None]
+ existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
@@ -285,7 +282,7 @@ def _syncTemplateRolesToDb() -> int:
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
# Ensure AccessRules exist for this role
- _ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
# Create new template role
newRole = Role(
@@ -296,11 +293,11 @@ def _syncTemplateRolesToDb() -> int:
featureInstanceId=None,
isSystemRole=False
)
- createdRole = db.recordCreate(Role, newRole.model_dump())
+ createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
# Create AccessRules for this role
- _ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
+ _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
@@ -309,7 +306,7 @@ def _syncTemplateRolesToDb() -> int:
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
# Repair instance-specific roles that are missing AccessRules
- _repairInstanceRolesAccessRules(db, existingRoleLabels)
+ _repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
return createdCount
@@ -318,13 +315,13 @@ def _syncTemplateRolesToDb() -> int:
return 0
-def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> int:
+def _repairInstanceRolesAccessRules(rootInterface, templateRoleLabels: Dict[str, str]) -> int:
"""
Repair instance-specific roles by copying AccessRules from their template roles.
This ensures instance roles created before AccessRules were defined get updated.
Args:
- db: Database connector
+ rootInterface: Root interface instance
templateRoleLabels: Dict mapping roleLabel to template role ID
Returns:
@@ -334,41 +331,41 @@ def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> i
repairedCount = 0
- # Get all instance-specific roles for this feature (mandateId is NOT None)
- allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
- instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
+ # Get all instance-specific roles for this feature (Pydantic models)
+ allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
+ instanceRoles = [r for r in allRoles if r.mandateId is not None]
for instanceRole in instanceRoles:
- roleLabel = instanceRole.get("roleLabel")
- instanceRoleId = instanceRole.get("id")
+ roleLabel = instanceRole.roleLabel
+ instanceRoleId = str(instanceRole.id)
# Find matching template role
templateRoleId = templateRoleLabels.get(roleLabel)
if not templateRoleId:
continue
- # Check if instance role has AccessRules
- existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
+ # Check if instance role has AccessRules (Pydantic models)
+ existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
if existingRules:
continue # Already has rules, skip
- # Copy AccessRules from template role
- templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
+ # Copy AccessRules from template role (Pydantic models)
+ templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
if not templateRules:
continue # Template has no rules
for rule in templateRules:
newRule = AccessRule(
roleId=instanceRoleId,
- context=rule.get("context"),
- item=rule.get("item"),
- view=rule.get("view", False),
- read=rule.get("read"),
- create=rule.get("create"),
- update=rule.get("update"),
- delete=rule.get("delete"),
+ context=rule.context,
+ item=rule.item,
+ view=rule.view if rule.view else False,
+ read=rule.read,
+ create=rule.create,
+ update=rule.update,
+ delete=rule.delete,
)
- db.recordCreate(AccessRule, newRule.model_dump())
+ rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
repairedCount += 1
@@ -379,12 +376,12 @@ def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> i
return repairedCount
-def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
+def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
"""
Ensure AccessRules exist for a role based on templates.
Args:
- db: Database connector
+ rootInterface: Root interface instance
roleId: Role ID
ruleTemplates: List of rule templates
@@ -393,13 +390,13 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any
"""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
- # Get existing rules for this role
- existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
+ # Get existing rules for this role (Pydantic models)
+ existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
existingSignatures = set()
for rule in existingRules:
- sig = (rule.get("context"), rule.get("item"))
+ sig = (str(rule.context) if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0
@@ -431,7 +428,7 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any
update=template.get("update"),
delete=template.get("delete"),
)
- db.recordCreate(AccessRule, newRule.model_dump())
+ rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
if createdCount > 0:
diff --git a/modules/features/trustee/routeFeatureTrustee.py b/modules/features/trustee/routeFeatureTrustee.py
index 9b1b1fca..43706a10 100644
--- a/modules/features/trustee/routeFeatureTrustee.py
+++ b/modules/features/trustee/routeFeatureTrustee.py
@@ -1363,17 +1363,11 @@ async def get_instance_roles(
rootInterface = getRootInterface()
- # Get instance-specific roles (mandateId set, featureInstanceId matches)
- roles = rootInterface.db.getRecordset(
- Role,
- recordFilter={
- "featureCode": "trustee",
- "featureInstanceId": instanceId
- }
- )
+ # Get instance-specific roles (Pydantic models)
+ roles = rootInterface.getRolesByFeatureCode("trustee", featureInstanceId=instanceId)
return PaginatedResponse(
- items=roles,
+ items=[r.model_dump() for r in roles],
pagination=None
)
@@ -1390,18 +1384,16 @@ async def get_instance_role(
mandateId = await _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface()
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
+ role = rootInterface.getRole(roleId)
- if not roles:
+ if not role:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
- role = roles[0]
-
# Verify role belongs to this instance
- if role.get("featureInstanceId") != instanceId:
+ if str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
- return role
+ return role.model_dump()
@router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse)
@@ -1420,19 +1412,16 @@ async def get_instance_role_rules(
rootInterface = getRootInterface()
- # Verify role belongs to this instance
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roles or roles[0].get("featureInstanceId") != instanceId:
+ # Verify role belongs to this instance (Pydantic model)
+ role = rootInterface.getRole(roleId)
+ if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
- # Get AccessRules for this role
- rules = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": roleId}
- )
+ # Get AccessRules for this role (Pydantic models)
+ rules = rootInterface.getAccessRulesByRole(roleId)
return PaginatedResponse(
- items=rules,
+ items=[r.model_dump() for r in rules],
pagination=None
)
@@ -1454,9 +1443,9 @@ async def create_instance_role_rule(
rootInterface = getRootInterface()
- # Verify role belongs to this instance
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roles or roles[0].get("featureInstanceId") != instanceId:
+ # Verify role belongs to this instance (Pydantic model)
+ role = rootInterface.getRole(roleId)
+ if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
# Create the rule
@@ -1505,14 +1494,14 @@ async def update_instance_role_rule(
rootInterface = getRootInterface()
- # Verify role belongs to this instance
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roles or roles[0].get("featureInstanceId") != instanceId:
+ # Verify role belongs to this instance (Pydantic model)
+ role = rootInterface.getRole(roleId)
+ if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
- # Verify rule belongs to role
- existingRules = rootInterface.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
- if not existingRules or existingRules[0].get("roleId") != roleId:
+ # Verify rule belongs to role (Pydantic model)
+ existingRule = rootInterface.getAccessRule(ruleId)
+ if not existingRule or str(existingRule.roleId) != roleId:
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
# Update only allowed fields
@@ -1529,7 +1518,7 @@ async def update_instance_role_rule(
updateData["delete"] = ruleData["delete"]
if not updateData:
- return existingRules[0]
+ return existingRule.model_dump()
try:
updated = rootInterface.db.recordModify(AccessRule, ruleId, updateData)
@@ -1556,14 +1545,14 @@ async def delete_instance_role_rule(
rootInterface = getRootInterface()
- # Verify role belongs to this instance
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roles or roles[0].get("featureInstanceId") != instanceId:
+ # Verify role belongs to this instance (Pydantic model)
+ role = rootInterface.getRole(roleId)
+ if not role or str(role.featureInstanceId) != instanceId:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
- # Verify rule belongs to role
- existingRules = rootInterface.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
- if not existingRules or existingRules[0].get("roleId") != roleId:
+ # Verify rule belongs to role (Pydantic model)
+ existingRule = rootInterface.getAccessRule(ruleId)
+ if not existingRule or str(existingRule.roleId) != roleId:
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
try:
diff --git a/modules/interfaces/interfaceAiObjects.py b/modules/interfaces/interfaceAiObjects.py
index 5c252ff6..2e6e36f5 100644
--- a/modules/interfaces/interfaceAiObjects.py
+++ b/modules/interfaces/interfaceAiObjects.py
@@ -97,7 +97,7 @@ class AiObjects:
return AiCallResponse(
content=errorMsg,
modelName="error",
- priceUsd=0.0,
+ priceCHF=0.0,
processingTime=0.0,
bytesSent=0,
bytesReceived=0,
@@ -135,7 +135,7 @@ class AiObjects:
return AiCallResponse(
content=errorMsg,
modelName="error",
- priceUsd=0.0,
+ priceCHF=0.0,
processingTime=0.0,
bytesSent=0,
bytesReceived=0,
@@ -147,7 +147,7 @@ class AiObjects:
return AiCallResponse(
content=errorMsg,
modelName="error",
- priceUsd=0.0,
+ priceCHF=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=outputBytes,
@@ -213,12 +213,12 @@ class AiObjects:
outputBytes = len(content.encode("utf-8"))
# Calculate price using model's own price calculation method
- priceUsd = model.calculatePriceUsd(processingTime, inputBytes, outputBytes)
+ priceCHF = model.calculatepriceCHF(processingTime, inputBytes, outputBytes)
return AiCallResponse(
content=content,
modelName=model.name,
- priceUsd=priceUsd,
+ priceCHF=priceCHF,
processingTime=processingTime,
bytesSent=inputBytes,
bytesReceived=outputBytes,
diff --git a/modules/interfaces/interfaceBootstrap.py b/modules/interfaces/interfaceBootstrap.py
index 3836d674..0b630f85 100644
--- a/modules/interfaces/interfaceBootstrap.py
+++ b/modules/interfaces/interfaceBootstrap.py
@@ -72,6 +72,10 @@ def initBootstrap(db: DatabaseConnector) -> None:
# Seed automation templates (after admin user exists)
initAutomationTemplates(db, adminUserId)
+
+ # Initialize feature instances for root mandate
+ if mandateId:
+ initRootMandateFeatures(db, mandateId)
def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str] = None) -> None:
@@ -153,6 +157,67 @@ def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str]
logger.info("System bootstrap completed")
+def initRootMandateFeatures(db: DatabaseConnector, mandateId: str) -> None:
+ """
+ Create feature instances for root mandate (chatplayground, automation).
+ These features are available to all users by default.
+
+ Args:
+ db: Database connector instance
+ mandateId: Root mandate ID
+ """
+ from modules.datamodels.datamodelFeatures import FeatureInstance
+ from modules.interfaces.interfaceFeatures import getFeatureInterface
+
+ logger.info("Initializing root mandate features")
+
+ # Features to create instances for
+ featuresToCreate = [
+ {"code": "chatplayground", "label": "Chat Playground"},
+ {"code": "automation", "label": "Automation"},
+ ]
+
+ featureInterface = getFeatureInterface(db)
+
+ for featureConfig in featuresToCreate:
+ featureCode = featureConfig["code"]
+ featureLabel = featureConfig["label"]
+
+ try:
+ # Check if instance already exists
+ existingInstances = db.getRecordset(
+ FeatureInstance,
+ recordFilter={
+ "mandateId": mandateId,
+ "featureCode": featureCode
+ }
+ )
+
+ if existingInstances:
+ logger.info(f"Feature instance for '{featureCode}' already exists in root mandate")
+ continue
+
+ # Create feature instance with template roles copied
+ instance = featureInterface.createFeatureInstance(
+ featureCode=featureCode,
+ mandateId=mandateId,
+ label=featureLabel,
+ enabled=True,
+ copyTemplateRoles=True
+ )
+
+ if instance:
+ instanceId = instance.get("id") if isinstance(instance, dict) else instance.id
+ logger.info(f"Created feature instance '{instanceId}' for '{featureCode}' in root mandate")
+ else:
+ logger.warning(f"Failed to create feature instance for '{featureCode}'")
+
+ except Exception as e:
+ logger.error(f"Error creating feature instance for '{featureCode}': {e}")
+
+ logger.info("Root mandate features initialization completed")
+
+
def initRootMandate(db: DatabaseConnector) -> Optional[str]:
"""
Creates the Root mandate if it doesn't exist.
diff --git a/modules/interfaces/interfaceDbApp.py b/modules/interfaces/interfaceDbApp.py
index 1f1d1e53..2a872bce 100644
--- a/modules/interfaces/interfaceDbApp.py
+++ b/modules/interfaces/interfaceDbApp.py
@@ -45,6 +45,7 @@ from modules.datamodels.datamodelMembership import (
)
from modules.datamodels.datamodelFeatures import Feature, FeatureInstance
from modules.datamodels.datamodelInvitation import Invitation
+from modules.datamodels.datamodelNotification import UserNotification
logger = logging.getLogger(__name__)
@@ -733,6 +734,9 @@ class AppObjects:
# Clear cache to ensure fresh data (already done above)
+ # Grant access to root mandate features (chatplayground, automation)
+ self._grantRootMandateFeatureAccess(createdUser[0]["id"])
+
return User(**createdUser[0])
except ValueError as e:
@@ -796,6 +800,99 @@ class AppObjects:
logger.error(f"Error updating user: {str(e)}")
raise ValueError(f"Failed to update user: {str(e)}")
+ def _grantRootMandateFeatureAccess(self, userId: str) -> None:
+ """
+ Grant a new user access to root mandate features (chatplayground, automation).
+ Creates FeatureAccess with viewer role for each feature instance.
+
+ Args:
+ userId: User ID to grant access to
+ """
+ try:
+ from modules.datamodels.datamodelFeatures import FeatureInstance
+ from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
+ from modules.datamodels.datamodelRbac import Role
+
+ # Get root mandate ID (first mandate in system)
+ allMandates = self.db.getRecordset(Mandate)
+ if not allMandates:
+ logger.debug("No mandates found, skipping feature access grant")
+ return
+ rootMandateId = allMandates[0].get("id")
+
+ # Feature codes to grant access to
+ rootFeatureCodes = ["chatplayground", "automation"]
+
+ # Get feature instances for root mandate
+ allInstances = self.db.getRecordset(FeatureInstance)
+ featureInstances = [
+ inst for inst in allInstances
+ if inst.get("mandateId") == rootMandateId
+ and inst.get("featureCode") in rootFeatureCodes
+ and inst.get("enabled") == True
+ ]
+
+ if not featureInstances:
+ logger.debug("No root mandate feature instances found, skipping feature access grant")
+ return
+
+ # Grant access to each feature instance
+ for instance in featureInstances:
+ instanceId = instance.get("id")
+ featureCode = instance.get("featureCode")
+
+ # Check if user already has access
+ existingAccess = self.db.getRecordset(
+ FeatureAccess,
+ recordFilter={
+ "userId": userId,
+ "featureInstanceId": instanceId
+ }
+ )
+
+ if existingAccess:
+ logger.debug(f"User {userId} already has access to feature instance {instanceId}")
+ continue
+
+ # Create FeatureAccess
+ featureAccess = FeatureAccess(
+ userId=userId,
+ featureInstanceId=instanceId,
+ enabled=True
+ )
+ createdAccess = self.db.recordCreate(FeatureAccess, featureAccess.model_dump())
+
+ if not createdAccess:
+ logger.warning(f"Failed to create FeatureAccess for user {userId} to instance {instanceId}")
+ continue
+
+ featureAccessId = createdAccess.get("id")
+
+ # Get viewer role for this feature instance
+ allRoles = self.db.getRecordset(Role)
+ viewerRoles = [
+ r for r in allRoles
+ if r.get("featureInstanceId") == instanceId
+ and r.get("roleLabel") == "viewer"
+ ]
+
+ if viewerRoles:
+ # Create FeatureAccessRole junction
+ featureAccessRole = FeatureAccessRole(
+ featureAccessId=featureAccessId,
+ roleId=viewerRoles[0].get("id")
+ )
+ self.db.recordCreate(FeatureAccessRole, featureAccessRole.model_dump())
+ logger.debug(f"Granted viewer role for {featureCode} to user {userId}")
+ else:
+ logger.warning(f"No viewer role found for feature instance {instanceId} ({featureCode})")
+
+ logger.info(f"Granted root mandate feature access to user {userId}")
+
+ except Exception as e:
+ # Log but don't fail user creation
+ logger.error(f"Error granting root mandate feature access to user {userId}: {e}")
+
def disableUser(self, userId: str) -> User:
"""Disables a user if current user has permission."""
return self.updateUser(userId, {"enabled": False})
@@ -1209,6 +1306,31 @@ class AppObjects:
logger.error(f"Error getting user connections: {str(e)}")
return []
+ def getUserConnectionById(self, connectionId: str) -> Optional[UserConnection]:
+ """Get a single UserConnection by ID."""
+ try:
+ connections = self.db.getRecordset(
+ UserConnection, recordFilter={"id": connectionId}
+ )
+ if connections:
+ conn_dict = connections[0]
+ return UserConnection(
+ id=conn_dict["id"],
+ userId=conn_dict["userId"],
+ authority=conn_dict.get("authority"),
+ externalId=conn_dict.get("externalId", ""),
+ externalUsername=conn_dict.get("externalUsername", ""),
+ externalEmail=conn_dict.get("externalEmail"),
+ status=conn_dict.get("status", "pending"),
+ connectedAt=conn_dict.get("connectedAt"),
+ lastChecked=conn_dict.get("lastChecked"),
+ expiresAt=conn_dict.get("expiresAt"),
+ )
+ return None
+ except Exception as e:
+ logger.error(f"Error getting user connection by ID: {str(e)}")
+ return None
+
def addUserConnection(
self,
userId: str,
@@ -1547,6 +1669,106 @@ class AppObjects:
logger.error(f"Error deleting UserMandate: {e}")
raise ValueError(f"Failed to delete UserMandate: {e}")
+ def getUserMandatesByMandate(self, mandateId: str) -> List[UserMandate]:
+ """
+ Get all UserMandate records for a specific mandate.
+
+ Args:
+ mandateId: Mandate ID
+
+ Returns:
+ List of UserMandate objects
+ """
+ try:
+ records = self.db.getRecordset(
+ UserMandate,
+ recordFilter={"mandateId": mandateId}
+ )
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(UserMandate(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting UserMandates for mandate {mandateId}: {e}")
+ return []
+
+ def getUserMandateRoles(self, userMandateId: str) -> List[UserMandateRole]:
+ """
+ Get all UserMandateRole records for a UserMandate.
+
+ Args:
+ userMandateId: UserMandate ID
+
+ Returns:
+ List of UserMandateRole objects
+ """
+ try:
+ records = self.db.getRecordset(
+ UserMandateRole,
+ recordFilter={"userMandateId": userMandateId}
+ )
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(UserMandateRole(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting UserMandateRoles: {e}")
+ return []
+
+ def deleteUserMandateRoles(self, userMandateId: str) -> int:
+ """
+ Delete all role assignments for a UserMandate.
+
+ Args:
+ userMandateId: UserMandate ID
+
+ Returns:
+ Number of deleted role assignments
+ """
+ try:
+ records = self.db.getRecordset(
+ UserMandateRole,
+ recordFilter={"userMandateId": userMandateId}
+ )
+ deletedCount = 0
+ for record in records:
+ if self.db.recordDelete(UserMandateRole, record.get("id")):
+ deletedCount += 1
+ return deletedCount
+ except Exception as e:
+ logger.error(f"Error deleting UserMandateRoles: {e}")
+ return 0
+
+ def validateRoleForMandate(self, roleId: str, mandateId: str) -> Role:
+ """
+ Validate a role exists and belongs to the specified mandate (or is global).
+
+ Args:
+ roleId: Role ID to validate
+ mandateId: Mandate ID for context validation
+
+ Returns:
+ Role object if valid
+
+ Raises:
+ ValueError: If role not found or belongs to different mandate
+ """
+ role = self.getRole(roleId)
+ if not role:
+ raise ValueError(f"Role {roleId} not found")
+
+ # Check mandate scope
+ if role.mandateId and str(role.mandateId) != str(mandateId):
+ raise ValueError(f"Role {roleId} belongs to a different mandate")
+
+ # Check feature-instance scope (not allowed at mandate level)
+ if role.featureInstanceId:
+ raise ValueError(f"Role {roleId} is a feature-instance role and cannot be assigned at mandate level")
+
+ return role
+
def getRoleIdsForUserMandate(self, userMandateId: str) -> List[str]:
"""
Get all role IDs assigned to a UserMandate.
@@ -1688,6 +1910,30 @@ class AppObjects:
logger.error(f"Error getting FeatureAccesses: {e}")
return []
+ def getFeatureAccessesByInstance(self, featureInstanceId: str) -> List[FeatureAccess]:
+ """
+ Get all FeatureAccess records for a specific feature instance.
+
+ Args:
+ featureInstanceId: FeatureInstance ID
+
+ Returns:
+ List of FeatureAccess objects
+ """
+ try:
+ records = self.db.getRecordset(
+ FeatureAccess,
+ recordFilter={"featureInstanceId": featureInstanceId}
+ )
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(FeatureAccess(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting FeatureAccesses for instance {featureInstanceId}: {e}")
+ return []
+
def createFeatureAccess(self, userId: str, featureInstanceId: str, roleIds: List[str] = None) -> FeatureAccess:
"""
Create a FeatureAccess record (grant user access to feature instance).
@@ -1750,6 +1996,445 @@ class AppObjects:
logger.error(f"Error getting role IDs for FeatureAccess: {e}")
return []
+ def deleteFeatureAccessRoles(self, featureAccessId: str) -> int:
+ """
+ Delete all FeatureAccessRole records for a FeatureAccess.
+
+ Args:
+ featureAccessId: FeatureAccess ID
+
+ Returns:
+ Number of records deleted
+ """
+ try:
+ records = self.db.getRecordset(
+ FeatureAccessRole,
+ recordFilter={"featureAccessId": featureAccessId}
+ )
+ count = 0
+ for record in records:
+ recordId = record.get("id")
+ if recordId:
+ self.db.recordDelete(FeatureAccessRole, recordId)
+ count += 1
+ return count
+ except Exception as e:
+ logger.error(f"Error deleting FeatureAccessRoles for {featureAccessId}: {e}")
+ return 0
+
+ # ============================================
+ # Invitation Methods
+ # ============================================
+
+ def getInvitation(self, invitationId: str) -> Optional[Invitation]:
+ """
+ Get an invitation by ID.
+
+ Args:
+ invitationId: Invitation ID
+
+ Returns:
+ Invitation object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(Invitation, recordFilter={"id": invitationId})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return Invitation(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting invitation {invitationId}: {e}")
+ return None
+
+ def getInvitationByToken(self, token: str) -> Optional[Invitation]:
+ """
+ Get an invitation by token.
+
+ Args:
+ token: Invitation token
+
+ Returns:
+ Invitation object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(Invitation, recordFilter={"token": token})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return Invitation(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting invitation by token: {e}")
+ return None
+
+ def getInvitationsByMandate(self, mandateId: str) -> List[Invitation]:
+ """
+ Get all invitations for a mandate.
+
+ Args:
+ mandateId: Mandate ID
+
+ Returns:
+ List of Invitation objects
+ """
+ try:
+ records = self.db.getRecordset(Invitation, recordFilter={"mandateId": mandateId})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(Invitation(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting invitations for mandate {mandateId}: {e}")
+ return []
+
+ def getInvitationsByCreator(self, creatorId: str) -> List[Invitation]:
+ """
+ Get all invitations created by a user.
+
+ Args:
+ creatorId: User ID who created the invitations
+
+ Returns:
+ List of Invitation objects
+ """
+ try:
+ records = self.db.getRecordset(Invitation, recordFilter={"createdBy": creatorId})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(Invitation(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting invitations by creator {creatorId}: {e}")
+ return []
+
+ def getInvitationsByUsedBy(self, usedById: str) -> List[Invitation]:
+ """
+ Get all invitations used by a user.
+
+ Args:
+ usedById: User ID who used the invitations
+
+ Returns:
+ List of Invitation objects
+ """
+ try:
+ records = self.db.getRecordset(Invitation, recordFilter={"usedBy": usedById})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(Invitation(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting invitations used by {usedById}: {e}")
+ return []
+
+ def getInvitationsByTargetUsername(self, targetUsername: str) -> List[Invitation]:
+ """
+ Get all invitations for a target username.
+
+ Args:
+ targetUsername: Target username for the invitations
+
+ Returns:
+ List of Invitation objects
+ """
+ try:
+ records = self.db.getRecordset(Invitation, recordFilter={"targetUsername": targetUsername})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(Invitation(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting invitations for target username {targetUsername}: {e}")
+ return []
+
+ # ============================================
+ # Additional Helper Methods
+ # ============================================
+
+ def getAllUsers(self) -> List[User]:
+ """
+ Get all users (for SysAdmin only).
+
+ Returns:
+ List of User objects (without sensitive fields)
+ """
+ try:
+ records = self.db.getRecordset(UserInDB)
+ result = []
+ for record in records:
+ # Filter out sensitive and internal fields
+ cleanedRecord = {
+ k: v for k, v in record.items()
+ if not k.startswith("_") and k not in ["hashedPassword", "resetToken", "resetTokenExpires"]
+ }
+ # Ensure roleLabels is a list
+ if cleanedRecord.get("roleLabels") is None:
+ cleanedRecord["roleLabels"] = []
+ result.append(User(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting all users: {e}")
+ return []
+
+ def getUserMandateById(self, userMandateId: str) -> Optional[UserMandate]:
+ """
+ Get a UserMandate by its ID.
+
+ Args:
+ userMandateId: UserMandate ID
+
+ Returns:
+ UserMandate object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(UserMandate, recordFilter={"id": userMandateId})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return UserMandate(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting UserMandate {userMandateId}: {e}")
+ return None
+
+ def getUserMandateRolesByRole(self, roleId: str) -> List[UserMandateRole]:
+ """
+ Get all UserMandateRole records for a specific role.
+
+ Args:
+ roleId: Role ID
+
+ Returns:
+ List of UserMandateRole objects
+ """
+ try:
+ records = self.db.getRecordset(UserMandateRole, recordFilter={"roleId": roleId})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(UserMandateRole(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting UserMandateRoles for role {roleId}: {e}")
+ return []
+
+ def getFeatureInstance(self, instanceId: str):
+ """
+ Get a FeatureInstance by ID.
+
+ Args:
+ instanceId: FeatureInstance ID
+
+ Returns:
+ FeatureInstance object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(FeatureInstance, recordFilter={"id": instanceId})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return FeatureInstance(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting FeatureInstance {instanceId}: {e}")
+ return None
+
+ def getFeatureByCode(self, featureCode: str) -> Optional[Feature]:
+ """
+ Get a Feature by its code.
+
+ Args:
+ featureCode: Feature code
+
+ Returns:
+ Feature object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(Feature, recordFilter={"code": featureCode})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return Feature(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting Feature by code {featureCode}: {e}")
+ return None
+
+ def getFeatureInstancesByMandate(self, mandateId: str, enabledOnly: bool = False) -> List[FeatureInstance]:
+ """
+ Get all FeatureInstances for a mandate.
+
+ Args:
+ mandateId: Mandate ID
+ enabledOnly: If True, only return enabled instances
+
+ Returns:
+ List of FeatureInstance objects
+ """
+ try:
+ recordFilter = {"mandateId": mandateId}
+ if enabledOnly:
+ recordFilter["enabled"] = True
+ records = self.db.getRecordset(FeatureInstance, recordFilter=recordFilter)
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(FeatureInstance(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting FeatureInstances for mandate {mandateId}: {e}")
+ return []
+
+ # ============================================
+ # Notification Methods
+ # ============================================
+
+ def getNotification(self, notificationId: str) -> Optional[UserNotification]:
+ """
+ Get a notification by ID.
+
+ Args:
+ notificationId: Notification ID
+
+ Returns:
+ UserNotification object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(UserNotification, recordFilter={"id": notificationId})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return UserNotification(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting notification {notificationId}: {e}")
+ return None
+
+ def getNotificationsByUser(
+ self,
+ userId: str,
+ status: Optional[str] = None,
+ limit: Optional[int] = None
+ ) -> List[UserNotification]:
+ """
+ Get notifications for a user.
+
+ Args:
+ userId: User ID
+ status: Optional status filter (e.g., 'unread')
+ limit: Optional limit on number of results
+
+ Returns:
+ List of UserNotification objects
+ """
+ try:
+ recordFilter = {"userId": userId}
+ if status:
+ recordFilter["status"] = status
+ records = self.db.getRecordset(UserNotification, recordFilter=recordFilter)
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(UserNotification(**cleanedRecord))
+ # Sort by createdAt descending
+ result.sort(key=lambda x: x.createdAt or 0, reverse=True)
+ if limit:
+ result = result[:limit]
+ return result
+ except Exception as e:
+ logger.error(f"Error getting notifications for user {userId}: {e}")
+ return []
+
+ # ============================================
+ # AccessRule Methods
+ # ============================================
+
+ def getAccessRule(self, ruleId: str) -> Optional[AccessRule]:
+ """
+ Get an AccessRule by ID.
+
+ Args:
+ ruleId: AccessRule ID
+
+ Returns:
+ AccessRule object if found, None otherwise
+ """
+ try:
+ records = self.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
+ if records:
+ cleanedRecord = {k: v for k, v in records[0].items() if not k.startswith("_")}
+ return AccessRule(**cleanedRecord)
+ return None
+ except Exception as e:
+ logger.error(f"Error getting AccessRule {ruleId}: {e}")
+ return None
+
+ def getAccessRulesByRole(self, roleId: str) -> List[AccessRule]:
+ """
+ Get all AccessRules for a role.
+
+ Args:
+ roleId: Role ID
+
+ Returns:
+ List of AccessRule objects
+ """
+ try:
+ records = self.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(AccessRule(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting AccessRules for role {roleId}: {e}")
+ return []
+
+ def getRolesByFeatureInstance(self, featureInstanceId: str) -> List[Role]:
+ """
+ Get all roles for a feature instance.
+
+ Args:
+ featureInstanceId: FeatureInstance ID
+
+ Returns:
+ List of Role objects
+ """
+ try:
+ records = self.db.getRecordset(Role, recordFilter={"featureInstanceId": featureInstanceId})
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(Role(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting roles for feature instance {featureInstanceId}: {e}")
+ return []
+
+ def getRolesByFeatureCode(self, featureCode: str, featureInstanceId: Optional[str] = None) -> List[Role]:
+ """
+ Get all roles for a feature code, optionally filtered by instance.
+
+ Args:
+ featureCode: Feature code
+ featureInstanceId: Optional FeatureInstance ID filter
+
+ Returns:
+ List of Role objects
+ """
+ try:
+ recordFilter = {"featureCode": featureCode}
+ if featureInstanceId:
+ recordFilter["featureInstanceId"] = featureInstanceId
+ records = self.db.getRecordset(Role, recordFilter=recordFilter)
+ result = []
+ for record in records:
+ cleanedRecord = {k: v for k, v in record.items() if not k.startswith("_")}
+ result.append(Role(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting roles for feature code {featureCode}: {e}")
+ return []
+
# Token methods
def saveAccessToken(self, token: Token, replace_existing: bool = True) -> None:
@@ -1908,6 +2593,56 @@ class AppObjects:
)
return None
+ def getTokensByConnectionIdAndAuthority(
+ self, connectionId: str, authority: AuthAuthority
+ ) -> List[Token]:
+ """Get tokens for a connection with specific authority."""
+ try:
+ tokens = self.db.getRecordset(
+ Token, recordFilter={
+ "connectionId": connectionId,
+ "authority": authority.value if hasattr(authority, 'value') else str(authority)
+ }
+ )
+ result = []
+ for token_dict in tokens:
+ cleanedRecord = {k: v for k, v in token_dict.items() if not k.startswith("_")}
+ result.append(Token(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting tokens by connection and authority: {str(e)}")
+ return []
+
+ def getTokensByUserIdNoConnection(
+ self, userId: str, authority: AuthAuthority
+ ) -> List[Token]:
+ """Get tokens for a user without a connection (access tokens)."""
+ try:
+ tokens = self.db.getRecordset(
+ Token, recordFilter={
+ "userId": userId,
+ "connectionId": None,
+ "authority": authority.value if hasattr(authority, 'value') else str(authority)
+ }
+ )
+ result = []
+ for token_dict in tokens:
+ cleanedRecord = {k: v for k, v in token_dict.items() if not k.startswith("_")}
+ result.append(Token(**cleanedRecord))
+ return result
+ except Exception as e:
+ logger.error(f"Error getting tokens by user and authority: {str(e)}")
+ return []
+
+ def getAllTokens(self, recordFilter: dict = None) -> List[dict]:
+ """Get all tokens with optional filtering (returns raw dicts)."""
+ try:
+ tokens = self.db.getRecordset(Token, recordFilter=recordFilter or {})
+ return tokens
+ except Exception as e:
+ logger.error(f"Error getting all tokens: {str(e)}")
+ return []
+
def findActiveTokenById(
self,
tokenId: str,
@@ -2340,6 +3075,42 @@ class AppObjects:
logger.error(f"Error getting role by label {roleLabel}: {str(e)}")
return None
+ def getRoleByLabelAndScope(
+ self,
+ roleLabel: str,
+ mandateId: Optional[str] = None,
+ featureInstanceId: Optional[str] = None,
+ featureCode: Optional[str] = None
+ ) -> Optional[Role]:
+ """
+ Get a role by label with scope filtering.
+
+ Args:
+ roleLabel: Role label
+ mandateId: Mandate ID (use None for global roles)
+ featureInstanceId: Feature instance ID
+ featureCode: Feature code
+
+ Returns:
+ Role object if found, None otherwise
+ """
+ try:
+ recordFilter = {"roleLabel": roleLabel}
+ if mandateId is not None:
+ recordFilter["mandateId"] = mandateId
+ if featureInstanceId is not None:
+ recordFilter["featureInstanceId"] = featureInstanceId
+ if featureCode is not None:
+ recordFilter["featureCode"] = featureCode
+
+ roles = self.db.getRecordset(Role, recordFilter=recordFilter)
+ if roles:
+ return Role(**roles[0])
+ return None
+ except Exception as e:
+ logger.error(f"Error getting role by label and scope {roleLabel}: {str(e)}")
+ return None
+
def getAllRoles(self, pagination: Optional[PaginationParams] = None) -> Union[List[Role], PaginatedResult]:
"""
Get all roles with optional pagination, sorting, and filtering.
diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py
index 84d2bfcf..87582b9e 100644
--- a/modules/routes/routeAdminFeatures.py
+++ b/modules/routes/routeAdminFeatures.py
@@ -204,38 +204,26 @@ async def get_my_feature_instances(
def _getUserRolesInInstance(rootInterface, userId: str, instanceId: str) -> List[str]:
"""Get all role labels for a user in a feature instance."""
try:
- from modules.datamodels.datamodelRbac import Role
- from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
+ # Get FeatureAccess for this user and instance (Pydantic model)
+ featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
- # Get FeatureAccess for this user and instance
- featureAccesses = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": userId, "featureInstanceId": instanceId}
- )
-
- if featureAccesses:
- featureAccessId = featureAccesses[0].get("id")
+ if featureAccess:
+ # Get role IDs via interface method
+ roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
- # Get role IDs via FeatureAccessRole junction table
- featureAccessRoles = rootInterface.db.getRecordset(
- FeatureAccessRole,
- recordFilter={"featureAccessId": featureAccessId}
- )
-
- if featureAccessRoles:
- # Get ALL roles, not just the first one
+ if roleIds:
+ # Get ALL roles and extract labels
roleLabels = []
- for far in featureAccessRoles:
- roleId = far.get("roleId")
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roles:
- roleLabels.append(roles[0].get("roleLabel", "user"))
+ for roleId in roleIds:
+ role = rootInterface.getRole(roleId)
+ if role:
+ roleLabels.append(role.roleLabel)
return roleLabels if roleLabels else ["user"]
- return ["user"] # Default
+ return ["user"] # Default - no access means basic user level
except Exception as e:
logger.debug(f"Error getting user roles: {e}")
- return ["user"]
+ return ["user"] # Fail-safe: default to basic user
def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict[str, Any]:
@@ -249,66 +237,53 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
}
try:
- from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role
- from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
+ from modules.datamodels.datamodelRbac import AccessRuleContext
- # Get FeatureAccess for this user and instance
- featureAccesses = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": userId, "featureInstanceId": instanceId}
- )
+ # Get FeatureAccess for this user and instance (Pydantic model)
+ featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
- logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccesses={len(featureAccesses) if featureAccesses else 0}")
+ logger.debug(f"_getInstancePermissions: userId={userId}, instanceId={instanceId}, featureAccess={featureAccess is not None}")
- if not featureAccesses:
+ if not featureAccess:
logger.debug(f"_getInstancePermissions: No FeatureAccess found for user {userId} and instance {instanceId}")
return permissions
- # Get role IDs via FeatureAccessRole junction table
- featureAccessId = featureAccesses[0].get("id")
- featureAccessRoles = rootInterface.db.getRecordset(
- FeatureAccessRole,
- recordFilter={"featureAccessId": featureAccessId}
- )
- roleIds = [far.get("roleId") for far in featureAccessRoles]
+ # Get role IDs via interface method
+ roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
- logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}")
+ logger.debug(f"_getInstancePermissions: featureAccessId={featureAccess.id}, roleIds={roleIds}")
if not roleIds:
- logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccessId}")
+ logger.debug(f"_getInstancePermissions: No roles found for FeatureAccess {featureAccess.id}")
return permissions
# Check if user has admin role
for roleId in roleIds:
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roles:
- roleLabel = roles[0].get("roleLabel", "").lower()
- if "admin" in roleLabel:
- permissions["isAdmin"] = True
- break
+ role = rootInterface.getRole(roleId)
+ if role and "admin" in role.roleLabel.lower():
+ permissions["isAdmin"] = True
+ break
# Get permissions (AccessRules) for all roles
for roleId in roleIds:
- accessRules = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": roleId}
- )
+ # Get all rules for this role (returns Pydantic models)
+ accessRules = rootInterface.getAccessRules(roleId=roleId)
logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
for rule in accessRules:
- context = rule.get("context", "")
- item = rule.get("item", "")
+ context = rule.context
+ item = rule.item or ""
# Handle DATA context (tables/fields)
- if context == "DATA" or context == AccessRuleContext.DATA:
+ if context == AccessRuleContext.DATA or context == "DATA":
if item:
# Check if it's a field (table.field) or table
if "." in item:
tableName, fieldName = item.split(".", 1)
if fieldName not in permissions["fields"]:
permissions["fields"][fieldName] = {"view": False}
- permissions["fields"][fieldName]["view"] = permissions["fields"][fieldName]["view"] or rule.get("view", False)
+ permissions["fields"][fieldName]["view"] = permissions["fields"][fieldName]["view"] or rule.view
else:
tableName = item
if tableName not in permissions["tables"]:
@@ -322,20 +297,18 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
# Merge permissions (highest wins)
current = permissions["tables"][tableName]
- current["view"] = current["view"] or rule.get("view", False)
- current["read"] = _mergeAccessLevel(current["read"], rule.get("read") or "n")
- current["create"] = _mergeAccessLevel(current["create"], rule.get("create") or "n")
- current["update"] = _mergeAccessLevel(current["update"], rule.get("update") or "n")
- current["delete"] = _mergeAccessLevel(current["delete"], rule.get("delete") or "n")
+ current["view"] = current["view"] or rule.view
+ current["read"] = _mergeAccessLevel(current["read"], rule.read or "n")
+ current["create"] = _mergeAccessLevel(current["create"], rule.create or "n")
+ current["update"] = _mergeAccessLevel(current["update"], rule.update or "n")
+ current["delete"] = _mergeAccessLevel(current["delete"], rule.delete or "n")
# Handle UI context (views)
- # Views are stored with full objectKey (e.g., ui.feature.trustee.dashboard)
- elif context == "UI" or context == AccessRuleContext.UI:
- ruleView = rule.get("view", False)
+ elif context == AccessRuleContext.UI or context == "UI":
if item:
# Store with full objectKey as per Navigation-API-Konzept
- permissions["views"][item] = permissions["views"].get(item, False) or ruleView
- elif ruleView:
+ permissions["views"][item] = permissions["views"].get(item, False) or rule.view
+ elif rule.view:
# item=None means all views - set a wildcard flag
permissions["views"]["_all"] = True
@@ -343,7 +316,7 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
except Exception as e:
logger.debug(f"Error getting instance permissions: {e}")
- return permissions
+ return permissions # Fail-safe: no permissions on error
def _mergeAccessLevel(current: str, new: str) -> str:
@@ -924,49 +897,35 @@ async def list_feature_instance_users(
detail="Access denied to this feature instance"
)
- # Get all FeatureAccess records for this instance
- from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
- from modules.datamodels.datamodelRbac import Role
-
- featureAccesses = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"featureInstanceId": instanceId}
- )
+ # Get all FeatureAccess records for this instance (Pydantic models)
+ featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
result = []
for fa in featureAccesses:
- userId = fa.get("userId")
- featureAccessId = fa.get("id")
-
- # Get user info
- users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": userId})
- if not users:
+ # Get user info (Pydantic model)
+ user = rootInterface.getUser(str(fa.userId))
+ if not user:
continue
- user = users[0]
- # Get role IDs via FeatureAccessRole junction table
- featureAccessRoles = rootInterface.db.getRecordset(
- FeatureAccessRole,
- recordFilter={"featureAccessId": featureAccessId}
- )
- roleIds = [far.get("roleId") for far in featureAccessRoles]
+ # Get role IDs via interface method
+ roleIds = rootInterface.getRoleIdsForFeatureAccess(str(fa.id))
# Get role labels
roleLabels = []
for roleId in roleIds:
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roles:
- roleLabels.append(roles[0].get("roleLabel", ""))
+ role = rootInterface.getRole(roleId)
+ if role:
+ roleLabels.append(role.roleLabel)
result.append(FeatureInstanceUserResponse(
- id=featureAccessId, # FeatureAccess ID as primary key
- userId=userId,
- username=user.get("username", ""),
- email=user.get("email"),
- fullName=user.get("fullName"),
+ id=str(fa.id), # FeatureAccess ID as primary key
+ userId=str(fa.userId),
+ username=user.username,
+ email=user.email,
+ fullName=user.fullName,
roleIds=roleIds,
roleLabels=roleLabels,
- enabled=fa.get("enabled", True)
+ enabled=fa.enabled
))
return result
@@ -1026,8 +985,8 @@ async def add_user_to_feature_instance(
)
# Verify user exists
- users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": data.userId})
- if not users:
+ user = rootInterface.getUser(data.userId)
+ if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User '{data.userId}' not found"
@@ -1035,10 +994,7 @@ async def add_user_to_feature_instance(
# Check if user already has access
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
- existingAccess = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": data.userId, "featureInstanceId": instanceId}
- )
+ existingAccess = rootInterface.getFeatureAccess(data.userId, instanceId)
if existingAccess:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
@@ -1131,17 +1087,14 @@ async def remove_user_from_feature_instance(
# Find FeatureAccess record
from modules.datamodels.datamodelMembership import FeatureAccess
- existingAccess = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": userId, "featureInstanceId": instanceId}
- )
+ existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
if not existingAccess:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have access to this feature instance"
)
- featureAccessId = existingAccess[0].get("id")
+ featureAccessId = str(existingAccess.id)
# Delete FeatureAccess (CASCADE will delete FeatureAccessRole records)
rootInterface.db.recordDelete(FeatureAccess, featureAccessId)
@@ -1215,29 +1168,21 @@ async def update_feature_instance_user_roles(
# Find FeatureAccess record
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
- existingAccess = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": userId, "featureInstanceId": instanceId}
- )
+ existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
if not existingAccess:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have access to this feature instance"
)
- featureAccessId = existingAccess[0].get("id")
+ featureAccessId = str(existingAccess.id)
# Update enabled flag if provided
if data.enabled is not None:
rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled})
- # Delete existing FeatureAccessRole records
- existingRoles = rootInterface.db.getRecordset(
- FeatureAccessRole,
- recordFilter={"featureAccessId": featureAccessId}
- )
- for role in existingRoles:
- rootInterface.db.recordDelete(FeatureAccessRole, role.get("id"))
+ # Delete existing FeatureAccessRole records via interface method
+ rootInterface.deleteFeatureAccessRoles(featureAccessId)
# Create new FeatureAccessRole records
for roleId in data.roleIds:
@@ -1304,21 +1249,17 @@ async def get_feature_instance_available_roles(
detail="Access denied to this feature instance"
)
- # Get roles for this instance
- from modules.datamodels.datamodelRbac import Role
- instanceRoles = rootInterface.db.getRecordset(
- Role,
- recordFilter={"featureInstanceId": instanceId}
- )
+ # Get roles for this instance using interface method
+ instanceRoles = rootInterface.getRolesByFeatureInstance(instanceId)
result = []
for role in instanceRoles:
result.append({
- "id": role.get("id"),
- "roleLabel": role.get("roleLabel"),
- "description": role.get("description", {}),
- "featureCode": role.get("featureCode"),
- "isSystemRole": role.get("isSystemRole", False)
+ "id": role.id,
+ "roleLabel": role.roleLabel,
+ "description": role.description or {},
+ "featureCode": role.featureCode,
+ "isSystemRole": role.isSystemRole
})
return result
@@ -1394,15 +1335,13 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
# Check if any of the user's roles is an admin role
try:
rootInterface = getRootInterface()
- from modules.datamodels.datamodelRbac import Role
for roleId in context.roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- roleLabel = role.get("roleLabel", "")
+ role = rootInterface.getRole(roleId)
+ if role:
+ roleLabel = role.roleLabel
# Admin role at mandate level (not feature-instance level)
- if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
+ if roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
diff --git a/modules/routes/routeAdminRbacExport.py b/modules/routes/routeAdminRbacExport.py
index 2164cb48..28caf8c8 100644
--- a/modules/routes/routeAdminRbacExport.py
+++ b/modules/routes/routeAdminRbacExport.py
@@ -85,34 +85,31 @@ async def export_global_rbac(
try:
rootInterface = getRootInterface()
- # Get all global template roles (mandateId is NULL)
- allRoles = rootInterface.db.getRecordset(Role)
- globalRoles = [r for r in allRoles if r.get("mandateId") is None]
+ # Get all global template roles (mandateId is NULL) using interface method
+ allRoles = rootInterface.getAllRoles()
+ globalRoles = [r for r in allRoles if r.mandateId is None]
exportRoles = []
for role in globalRoles:
- roleId = role.get("id")
+ roleId = role.id
- # Get access rules for this role
- accessRules = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": roleId}
- )
+ # Get access rules for this role using interface method
+ accessRules = rootInterface.getAccessRulesByRole(roleId)
exportRoles.append(RoleExport(
- roleLabel=role.get("roleLabel"),
- description=role.get("description", {}),
- featureCode=role.get("featureCode"),
- isSystemRole=role.get("isSystemRole", False),
+ roleLabel=role.roleLabel,
+ description=role.description or {},
+ featureCode=role.featureCode,
+ isSystemRole=role.isSystemRole,
accessRules=[
{
- "context": r.get("context"),
- "item": r.get("item"),
- "view": r.get("view", False),
- "read": r.get("read"),
- "create": r.get("create"),
- "update": r.get("update"),
- "delete": r.get("delete")
+ "context": r.context,
+ "item": r.item,
+ "view": r.view if r.view is not None else False,
+ "read": r.read,
+ "create": r.create,
+ "update": r.update,
+ "delete": r.delete
}
for r in accessRules
]
@@ -191,21 +188,20 @@ async def import_global_rbac(
result.rolesSkipped += 1
continue
- # Check if role exists (global role with same label and featureCode)
- existingRoles = rootInterface.db.getRecordset(
- Role,
- recordFilter={
- "roleLabel": roleLabel,
- "mandateId": None,
- "featureCode": featureCode
- }
- )
+ # Check if role exists (global role with same label and featureCode) using interface method
+ allRoles = rootInterface.getAllRoles()
+ existingRoles = [
+ r for r in allRoles
+ if r.roleLabel == roleLabel
+ and r.mandateId is None
+ and r.featureCode == featureCode
+ ]
if existingRoles:
if updateExisting:
# Update existing role
existingRole = existingRoles[0]
- roleId = existingRole.get("id")
+ roleId = existingRole.id
rootInterface.db.recordModify(
Role,
@@ -315,41 +311,38 @@ async def export_mandate_rbac(
try:
rootInterface = getRootInterface()
- # Get mandate-level roles
- allRoles = rootInterface.db.getRecordset(Role)
+ # Get mandate-level roles using interface method
+ allRoles = rootInterface.getAllRoles()
mandateRoles = [
r for r in allRoles
- if str(r.get("mandateId")) == str(context.mandateId)
+ if str(r.mandateId) == str(context.mandateId)
]
# Filter by feature instance if not including them
if not includeFeatureInstances:
- mandateRoles = [r for r in mandateRoles if not r.get("featureInstanceId")]
+ mandateRoles = [r for r in mandateRoles if not r.featureInstanceId]
exportRoles = []
for role in mandateRoles:
- roleId = role.get("id")
+ roleId = role.id
- # Get access rules for this role
- accessRules = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": roleId}
- )
+ # Get access rules for this role using interface method
+ accessRules = rootInterface.getAccessRulesByRole(roleId)
exportRoles.append(RoleExport(
- roleLabel=role.get("roleLabel"),
- description=role.get("description", {}),
- featureCode=role.get("featureCode"),
- isSystemRole=role.get("isSystemRole", False),
+ roleLabel=role.roleLabel,
+ description=role.description or {},
+ featureCode=role.featureCode,
+ isSystemRole=role.isSystemRole,
accessRules=[
{
- "context": r.get("context"),
- "item": r.get("item"),
- "view": r.get("view", False),
- "read": r.get("read"),
- "create": r.get("create"),
- "update": r.get("update"),
- "delete": r.get("delete")
+ "context": r.context,
+ "item": r.item,
+ "view": r.view if r.view is not None else False,
+ "read": r.read,
+ "create": r.create,
+ "update": r.update,
+ "delete": r.delete
}
for r in accessRules
]
@@ -453,21 +446,20 @@ async def import_mandate_rbac(
result.rolesSkipped += 1
continue
- # Check if role exists (mandate role with same label)
- existingRoles = rootInterface.db.getRecordset(
- Role,
- recordFilter={
- "roleLabel": roleLabel,
- "mandateId": str(context.mandateId),
- "featureInstanceId": None # Only mandate-level roles
- }
- )
+ # Check if role exists (mandate role with same label) using interface method
+ allRoles = rootInterface.getAllRoles()
+ existingRoles = [
+ r for r in allRoles
+ if r.roleLabel == roleLabel
+ and str(r.mandateId) == str(context.mandateId)
+ and r.featureInstanceId is None # Only mandate-level roles
+ ]
if existingRoles:
if updateExisting:
# Update existing role
existingRole = existingRoles[0]
- roleId = existingRole.get("id")
+ roleId = existingRole.id
rootInterface.db.recordModify(
Role,
@@ -556,12 +548,11 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
rootInterface = getRootInterface()
for roleId in context.roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- roleLabel = role.get("roleLabel", "")
+ role = rootInterface.getRole(roleId)
+ if role:
+ roleLabel = role.roleLabel
# Admin role at mandate level
- if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
+ if roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
@@ -580,10 +571,10 @@ def _updateAccessRules(interface, roleId: str, newRules: List[Dict[str, Any]]) -
Number of rules created/updated
"""
try:
- # Delete existing rules for this role
- existingRules = interface.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
+ # Delete existing rules for this role using interface method
+ existingRules = interface.getAccessRulesByRole(roleId)
for rule in existingRules:
- interface.db.recordDelete(AccessRule, rule.get("id"))
+ interface.db.recordDelete(AccessRule, rule.id)
# Create new rules
count = 0
diff --git a/modules/routes/routeAdminRbacRoles.py b/modules/routes/routeAdminRbacRoles.py
index ad8a0de5..75e00cd5 100644
--- a/modules/routes/routeAdminRbacRoles.py
+++ b/modules/routes/routeAdminRbacRoles.py
@@ -36,25 +36,17 @@ def _getUserRoleLabels(interface, userId: str) -> List[str]:
"""
roleLabels: Set[str] = set()
- # Get all UserMandate records for this user
- userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
+ # Get all UserMandate records for this user (Pydantic models)
+ userMandates = interface.getUserMandates(userId)
for um in userMandates:
- userMandateId = um.get("id")
- if not userMandateId:
- continue
-
- # Get all UserMandateRole records for this membership
- userMandateRoles = interface.db.getRecordset(
- UserMandateRole,
- recordFilter={"userMandateId": str(userMandateId)}
- )
+ # Get all UserMandateRole records for this membership (Pydantic models)
+ userMandateRoles = interface.getUserMandateRoles(str(um.id))
for umr in userMandateRoles:
- roleId = umr.get("roleId")
- if roleId:
+ if umr.roleId:
# Get role by ID to get roleLabel
- role = interface.getRole(str(roleId))
+ role = interface.getRole(str(umr.roleId))
if role:
roleLabels.add(role.roleLabel)
@@ -362,21 +354,13 @@ async def list_users_with_roles(
try:
interface = getRootInterface()
- # Get all users (SysAdmin sees all)
- # Use db.getRecordset with UserInDB (the actual database model)
- allUsersData = interface.db.getRecordset(UserInDB)
- # Convert to User objects, filtering out sensitive fields
- users = []
- for u in allUsersData:
- cleanedUser = {k: v for k, v in u.items() if not k.startswith("_") and k != "hashedPassword" and k != "resetToken" and k != "resetTokenExpires"}
- if cleanedUser.get("roleLabels") is None:
- cleanedUser["roleLabels"] = []
- users.append(User(**cleanedUser))
+ # Get all users via interface method (Pydantic models)
+ users = interface.getAllUsers()
# Filter by mandate if specified (via UserMandate table)
if mandateId:
- userMandates = interface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
- mandateUserIds = {str(um["userId"]) for um in userMandates}
+ userMandates = interface.getUserMandatesByMandate(mandateId)
+ mandateUserIds = {str(um.userId) for um in userMandates}
users = [u for u in users if str(u.id) in mandateUserIds]
# Filter by role if specified (via UserMandateRole)
@@ -499,21 +483,18 @@ async def update_user_roles(
logger.warning(f"Non-standard role label assigned: {roleLabel}")
# Get user's first mandate (for role assignment)
- userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
+ userMandates = interface.getUserMandates(userId)
if not userMandates:
raise HTTPException(
status_code=400,
detail=f"User {userId} has no mandate memberships. Add to mandate first."
)
- userMandateId = str(userMandates[0].get("id"))
+ userMandateId = str(userMandates[0].id)
- # Get current roles for this mandate
- existingRoles = interface.db.getRecordset(
- UserMandateRole,
- recordFilter={"userMandateId": userMandateId}
- )
- existingRoleIds = {str(r.get("roleId")) for r in existingRoles}
+ # Get current roles for this mandate (Pydantic models)
+ existingRoles = interface.getUserMandateRoles(userMandateId)
+ existingRoleIds = {str(r.roleId) for r in existingRoles}
# Convert roleLabels to roleIds
newRoleIds = set()
@@ -524,8 +505,8 @@ async def update_user_roles(
# Remove roles that are no longer needed
for existingRole in existingRoles:
- if str(existingRole.get("roleId")) not in newRoleIds:
- interface.db.recordDelete(UserMandateRole, str(existingRole.get("id")))
+ if str(existingRole.roleId) not in newRoleIds:
+ interface.removeRoleFromUserMandate(userMandateId, str(existingRole.roleId))
# Add new roles
for roleId in newRoleIds:
@@ -596,25 +577,22 @@ async def add_user_role(
)
# Get user's first mandate
- userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
+ userMandates = interface.getUserMandates(userId)
if not userMandates:
raise HTTPException(
status_code=400,
detail=f"User {userId} has no mandate memberships. Add to mandate first."
)
- userMandateId = str(userMandates[0].get("id"))
+ userMandateId = str(userMandates[0].id)
- # Check if role is already assigned
- existingAssignment = interface.db.getRecordset(
- UserMandateRole,
- recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
- )
+ # Check if role is already assigned - use interface method
+ existingRoles = interface.getUserMandateRoles(userMandateId)
+ roleAlreadyAssigned = any(str(r.roleId) == str(role.id) for r in existingRoles)
- if not existingAssignment:
- # Add the role
- newRole = UserMandateRole(userMandateId=userMandateId, roleId=str(role.id))
- interface.db.recordCreate(UserMandateRole, newRole.model_dump())
+ if not roleAlreadyAssigned:
+ # Add the role via interface method
+ interface.addRoleToUserMandate(userMandateId, str(role.id))
logger.info(f"Added role {roleLabel} to user {userId} by SysAdmin {currentUser.id}")
userRoleLabels = _getUserRoleLabels(interface, userId)
@@ -678,20 +656,14 @@ async def remove_user_role(
)
# Remove role from all user's mandates
- userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
+ userMandates = interface.getUserMandates(userId)
roleRemoved = False
for um in userMandates:
- userMandateId = str(um.get("id"))
+ userMandateId = str(um.id)
- # Find and delete the role assignment
- assignments = interface.db.getRecordset(
- UserMandateRole,
- recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
- )
-
- for assignment in assignments:
- interface.db.recordDelete(UserMandateRole, str(assignment.get("id")))
+ # Remove role via interface method
+ if interface.removeRoleFromUserMandate(userMandateId, str(role.id)):
roleRemoved = True
if roleRemoved:
@@ -751,25 +723,21 @@ async def get_users_with_role(
detail=f"Role '{roleLabel}' not found"
)
- # Get all UserMandateRole assignments for this role
- roleAssignments = interface.db.getRecordset(
- UserMandateRole,
- recordFilter={"roleId": str(role.id)}
- )
+ # Get all UserMandateRole assignments for this role (Pydantic models)
+ roleAssignments = interface.getUserMandateRolesByRole(str(role.id))
# Get unique userMandateIds
- userMandateIds = {str(ra.get("userMandateId")) for ra in roleAssignments}
+ userMandateIds = {str(ra.userMandateId) for ra in roleAssignments}
# Get userIds from UserMandate records
userIds: Set[str] = set()
for userMandateId in userMandateIds:
- umRecords = interface.db.getRecordset(UserMandate, recordFilter={"id": userMandateId})
- if umRecords:
- um = umRecords[0]
+ um = interface.getUserMandateById(userMandateId)
+ if um:
# Filter by mandate if specified
- if mandateId and str(um.get("mandateId")) != mandateId:
+ if mandateId and str(um.mandateId) != mandateId:
continue
- userIds.add(str(um.get("userId")))
+ userIds.add(str(um.userId))
# Get users and format response
result = []
diff --git a/modules/routes/routeAdminRbacRules.py b/modules/routes/routeAdminRbacRules.py
index 916caf38..fc9b315e 100644
--- a/modules/routes/routeAdminRbacRules.py
+++ b/modules/routes/routeAdminRbacRules.py
@@ -179,17 +179,15 @@ async def get_all_permissions(
# For UI/RESOURCE: Load system roles the user has across ALL their mandates
# This allows users to access system UI elements without needing a specific mandate header
- userMandates = rootInterface.db.getRecordset(
- UserMandate,
- recordFilter={"userId": str(reqContext.user.id), "enabled": True}
- )
+ allUserMandates = rootInterface.getUserMandates(str(reqContext.user.id))
+ userMandates = [um for um in allUserMandates if um.enabled]
logger.debug(f"UI/RESOURCE permissions: Found {len(userMandates)} UserMandates for user {reqContext.user.id}")
# Collect all role IDs the user has across all mandates
for userMandate in userMandates:
- mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.get("id"))
- logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.get('id')} (mandate {userMandate.get('mandateId')}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
+ mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.id)
+ logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.id} (mandate {userMandate.mandateId}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
for rid in mandateRoleIds:
if rid not in roleIds:
roleIds.append(rid)
@@ -210,14 +208,11 @@ async def get_all_permissions(
allRules[ctx] = []
# Get all rules for user's roles - bypass RBAC filtering
for roleId in roleIds:
- ruleRecords = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": str(roleId), "context": ctx.value}
- )
- for ruleRecord in ruleRecords:
- # Convert dict to AccessRule object
- cleanedRule = {k: v for k, v in ruleRecord.items() if not k.startswith("_")}
- allRules[ctx].append(AccessRule(**cleanedRule))
+ # Use interface method and filter by context
+ rules = rootInterface.getAccessRulesByRole(str(roleId))
+ for rule in rules:
+ if rule.context == ctx.value:
+ allRules[ctx].append(rule)
# Build result: for each context, collect all unique items and calculate permissions
for ctx in contextsToFetch:
@@ -405,14 +400,8 @@ async def get_access_rules_by_role(
try:
interface = getRootInterface()
- # Build filter for roleId
- recordFilter = {"roleId": roleId}
-
- # Get rules from database
- rules = interface.db.getRecordset(AccessRule, recordFilter=recordFilter)
-
- # Convert to AccessRule objects
- ruleObjects = [AccessRule(**rule) for rule in rules]
+ # Get rules from database using interface method
+ ruleObjects = interface.getAccessRulesByRole(roleId)
return PaginatedResponse(
items=[rule.model_dump() for rule in ruleObjects],
@@ -1128,13 +1117,9 @@ async def getCatalogObjects(
if mandateId:
try:
interface = getRootInterface()
- # Get all feature instances for this mandate
- from modules.datamodels.datamodelFeatures import FeatureInstance
- instances = interface.db.getRecordset(
- FeatureInstance,
- recordFilter={"mandateId": mandateId, "enabled": True}
- )
- activeFeatures = set(inst.get("featureCode") for inst in instances)
+ # Get all feature instances for this mandate using interface method
+ instances = interface.getFeatureInstancesByMandate(mandateId, enabledOnly=True)
+ activeFeatures = set(inst.featureCode for inst in instances)
# Always include "system" feature
activeFeatures.add("system")
except Exception as e:
diff --git a/modules/routes/routeAdminUserAccessOverview.py b/modules/routes/routeAdminUserAccessOverview.py
index f12fe2b6..372e2193 100644
--- a/modules/routes/routeAdminUserAccessOverview.py
+++ b/modules/routes/routeAdminUserAccessOverview.py
@@ -47,11 +47,15 @@ def _getAccessLevelLabel(level: Optional[str]) -> str:
return labels.get(level, "-")
-def _getRoleScope(role: Dict[str, Any]) -> str:
- """Determine the scope of a role."""
- if role.get("featureInstanceId"):
+def _getRoleScope(role) -> str:
+ """Determine the scope of a role. Accepts Role object or dict."""
+ # Support both Pydantic models and dicts
+ featureInstanceId = getattr(role, 'featureInstanceId', None) or (role.get("featureInstanceId") if isinstance(role, dict) else None)
+ mandateId = getattr(role, 'mandateId', None) or (role.get("mandateId") if isinstance(role, dict) else None)
+
+ if featureInstanceId:
return "instance"
- elif role.get("mandateId"):
+ elif mandateId:
return "mandate"
else:
return "global"
@@ -79,18 +83,18 @@ async def listUsersForOverview(
try:
interface = getRootInterface()
- # Get all users
- allUsersData = interface.db.getRecordset(UserInDB)
+ # Get all users using interface method
+ allUsers = interface.getAllUsers()
result = []
- for u in allUsersData:
+ for u in allUsers:
result.append({
- "id": u.get("id"),
- "username": u.get("username"),
- "email": u.get("email"),
- "fullName": u.get("fullName"),
- "isSysAdmin": u.get("isSysAdmin", False),
- "enabled": u.get("enabled", True),
+ "id": u.id,
+ "username": u.username,
+ "email": u.email,
+ "fullName": u.fullName,
+ "isSysAdmin": u.isSysAdmin,
+ "enabled": u.enabled,
})
# Sort by username
@@ -172,47 +176,43 @@ async def getUserAccessOverview(
allRoles = []
roleIdToInfo = {} # Map roleId to role info for later reference
- # Get mandates for this user
- mandateFilter = {"userId": userId, "enabled": True}
+ # Get mandates for this user using interface method
+ allUserMandates = interface.getUserMandates(userId)
+ # Filter by enabled and optionally mandateId
+ userMandates = [um for um in allUserMandates if um.enabled]
if mandateId:
- mandateFilter["mandateId"] = mandateId
-
- userMandates = interface.db.getRecordset(UserMandate, recordFilter=mandateFilter)
+ userMandates = [um for um in userMandates if um.mandateId == mandateId]
mandatesInfo = []
for um in userMandates:
- umId = um.get("id")
- umMandateId = um.get("mandateId")
+ umId = um.id
+ umMandateId = um.mandateId
# Get mandate name
mandate = interface.getMandate(umMandateId)
mandateName = mandate.name if mandate else umMandateId
- # Get roles for this UserMandate
- umRoles = interface.db.getRecordset(
- UserMandateRole,
- recordFilter={"userMandateId": umId}
- )
+ # Get roles for this UserMandate using interface method
+ umRoles = interface.getUserMandateRoles(umId)
mandateRoleIds = []
for umr in umRoles:
- roleId = umr.get("roleId")
+ roleId = umr.roleId
if roleId:
mandateRoleIds.append(roleId)
- # Get role details
- roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
+ # Get role details using interface method
+ role = interface.getRole(roleId)
+ if role:
scope = _getRoleScope(role)
roleInfo = {
"id": roleId,
- "roleLabel": role.get("roleLabel"),
- "description": role.get("description", {}),
+ "roleLabel": role.roleLabel,
+ "description": role.description or {},
"scope": scope,
"scopePriority": _getRoleScopePriority(scope),
- "mandateId": role.get("mandateId"),
- "featureInstanceId": role.get("featureInstanceId"),
+ "mandateId": role.mandateId,
+ "featureInstanceId": role.featureInstanceId,
"source": "mandate",
"sourceMandateId": umMandateId,
"sourceMandateName": mandateName,
@@ -220,69 +220,59 @@ async def getUserAccessOverview(
allRoles.append(roleInfo)
roleIdToInfo[roleId] = roleInfo
- # Get feature instances for this mandate
- featureInstanceFilter = {"userId": userId, "enabled": True}
- featureAccesses = interface.db.getRecordset(FeatureAccess, recordFilter=featureInstanceFilter)
+ # Get feature instances for this mandate using interface method
+ allFeatureAccesses = interface.getFeatureAccessesForUser(userId)
+ featureAccesses = [fa for fa in allFeatureAccesses if fa.enabled]
featureInstancesInfo = []
for fa in featureAccesses:
- faId = fa.get("id")
- faInstanceId = fa.get("featureInstanceId")
+ faId = fa.id
+ faInstanceId = fa.featureInstanceId
- # Check if instance belongs to this mandate
- instance = interface.db.getRecordset(FeatureInstance, recordFilter={"id": faInstanceId})
+ # Check if instance belongs to this mandate using interface method
+ instance = interface.getFeatureInstance(faInstanceId)
if not instance:
continue
- instance = instance[0]
- if instance.get("mandateId") != umMandateId:
+ if instance.mandateId != umMandateId:
continue
# Filter by featureInstanceId if specified
if featureInstanceId and faInstanceId != featureInstanceId:
continue
- # Get feature info
- featureCode = instance.get("featureCode")
- featureRecords = interface.db.getRecordset(Feature, recordFilter={"code": featureCode})
- featureLabel = featureRecords[0].get("label", {}) if featureRecords else {}
+ # Get feature info using interface method
+ featureCode = instance.featureCode
+ feature = interface.getFeatureByCode(featureCode)
+ featureLabel = feature.label if feature else {}
- # Get roles for this FeatureAccess
- faRoles = interface.db.getRecordset(
- FeatureAccessRole,
- recordFilter={"featureAccessId": faId}
- )
+ # Get roles for this FeatureAccess using interface method
+ instanceRoleIds = interface.getRoleIdsForFeatureAccess(faId)
- instanceRoleIds = []
- for far in faRoles:
- roleId = far.get("roleId")
- if roleId:
- instanceRoleIds.append(roleId)
-
- # Get role details (if not already added)
- if roleId not in roleIdToInfo:
- roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- scope = _getRoleScope(role)
- roleInfo = {
- "id": roleId,
- "roleLabel": role.get("roleLabel"),
- "description": role.get("description", {}),
- "scope": scope,
- "scopePriority": _getRoleScopePriority(scope),
- "mandateId": role.get("mandateId"),
- "featureInstanceId": role.get("featureInstanceId"),
- "source": "featureInstance",
- "sourceInstanceId": faInstanceId,
- "sourceInstanceLabel": instance.get("label"),
- }
- allRoles.append(roleInfo)
- roleIdToInfo[roleId] = roleInfo
+ for roleId in instanceRoleIds:
+ # Get role details (if not already added)
+ if roleId not in roleIdToInfo:
+ role = interface.getRole(roleId)
+ if role:
+ scope = _getRoleScope(role)
+ roleInfo = {
+ "id": roleId,
+ "roleLabel": role.roleLabel,
+ "description": role.description or {},
+ "scope": scope,
+ "scopePriority": _getRoleScopePriority(scope),
+ "mandateId": role.mandateId,
+ "featureInstanceId": role.featureInstanceId,
+ "source": "featureInstance",
+ "sourceInstanceId": faInstanceId,
+ "sourceInstanceLabel": instance.label,
+ }
+ allRoles.append(roleInfo)
+ roleIdToInfo[roleId] = roleInfo
featureInstancesInfo.append({
"id": faInstanceId,
- "label": instance.get("label"),
+ "label": instance.label,
"featureCode": featureCode,
"featureLabel": featureLabel,
"roleIds": instanceRoleIds,
@@ -317,12 +307,12 @@ async def getUserAccessOverview(
roleLabel = roleInfo.get("roleLabel", "unknown")
roleScope = roleInfo.get("scope", "unknown")
- # Get all rules for this role
- rules = interface.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
+ # Get all rules for this role using interface method
+ rules = interface.getAccessRulesByRole(roleId)
for rule in rules:
- context = rule.get("context")
- item = rule.get("item")
+ context = rule.context
+ item = rule.item
accessEntry = {
"item": item or "(all)",
@@ -333,20 +323,20 @@ async def getUserAccessOverview(
}
if context == "UI":
- accessEntry["view"] = rule.get("view", False)
+ accessEntry["view"] = rule.view if rule.view is not None else False
if accessEntry["view"]:
uiAccess.append(accessEntry)
elif context == "DATA":
- accessEntry["view"] = rule.get("view", False)
- accessEntry["read"] = _getAccessLevelLabel(rule.get("read"))
- accessEntry["create"] = _getAccessLevelLabel(rule.get("create"))
- accessEntry["update"] = _getAccessLevelLabel(rule.get("update"))
- accessEntry["delete"] = _getAccessLevelLabel(rule.get("delete"))
+ accessEntry["view"] = rule.view if rule.view is not None else False
+ accessEntry["read"] = _getAccessLevelLabel(rule.read)
+ accessEntry["create"] = _getAccessLevelLabel(rule.create)
+ accessEntry["update"] = _getAccessLevelLabel(rule.update)
+ accessEntry["delete"] = _getAccessLevelLabel(rule.delete)
dataAccess.append(accessEntry)
elif context == "RESOURCE":
- accessEntry["view"] = rule.get("view", False)
+ accessEntry["view"] = rule.view if rule.view is not None else False
if accessEntry["view"]:
resourceAccess.append(accessEntry)
diff --git a/modules/routes/routeChat.py b/modules/routes/routeChat.py
deleted file mode 100644
index 137b4a99..00000000
--- a/modules/routes/routeChat.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright (c) 2025 Patrick Motsch
-# All rights reserved.
-"""
-Chat Playground routes for the backend API.
-Implements the endpoints for chat playground workflow management.
-"""
-
-import logging
-from typing import Optional, Dict, Any
-from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Request
-
-# Import auth modules
-from modules.auth import limiter, getRequestContext, RequestContext
-
-# Import interfaces
-from modules.interfaces import interfaceDbChat
-
-# Import models
-from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
-
-# Import workflow control functions
-from modules.workflows.automation import chatStart, chatStop
-
-# Configure logger
-logger = logging.getLogger(__name__)
-
-# Create router for chat playground endpoints
-router = APIRouter(
- prefix="/api/chat/playground",
- tags=["Chat Playground"],
- responses={404: {"description": "Not found"}}
-)
-
-def _getServiceChat(context: RequestContext):
- return interfaceDbChat.getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
-
-# Workflow start endpoint
-@router.post("/start", response_model=ChatWorkflow)
-@limiter.limit("120/minute")
-async def start_workflow(
- request: Request,
- workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
- workflowMode: WorkflowModeEnum = Query(..., description="Workflow mode: 'Dynamic' or 'Automation' (mandatory)"),
- userInput: UserInputRequest = Body(...),
- context: RequestContext = Depends(getRequestContext)
-) -> ChatWorkflow:
- """
- Starts a new workflow or continues an existing one.
- Corresponds to State 1 in the state machine documentation.
-
- Args:
- workflowMode: "Dynamic" for iterative dynamic-style processing, "Automation" for automated workflow execution
- """
- try:
- # Start or continue workflow using playground controller
- mandateId = str(context.mandateId) if context.mandateId else None
- workflow = await chatStart(context.user, userInput, workflowMode, workflowId, mandateId=mandateId)
-
- return workflow
-
- except Exception as e:
- logger.error(f"Error in start_workflow: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=str(e)
- )
-
-# State 8: Workflow Stopped endpoint
-@router.post("/{workflowId}/stop", response_model=ChatWorkflow)
-@limiter.limit("120/minute")
-async def stop_workflow(
- request: Request,
- workflowId: str = Path(..., description="ID of the workflow to stop"),
- context: RequestContext = Depends(getRequestContext)
-) -> ChatWorkflow:
- """Stops a running workflow."""
- try:
- # Stop workflow using playground controller
- mandateId = str(context.mandateId) if context.mandateId else None
- workflow = await chatStop(context.user, workflowId, mandateId=mandateId)
-
- return workflow
-
- except Exception as e:
- logger.error(f"Error in stop_workflow: {str(e)}")
- raise HTTPException(
- status_code=500,
- detail=str(e)
- )
-
-# Unified Chat Data Endpoint for Polling
-@router.get("/{workflowId}/chatData")
-@limiter.limit("120/minute")
-async def get_workflow_chat_data(
- request: Request,
- workflowId: str = Path(..., description="ID of the workflow"),
- afterTimestamp: Optional[float] = Query(None, description="Unix timestamp to get data after"),
- context: RequestContext = Depends(getRequestContext)
-) -> Dict[str, Any]:
- """
- Get unified chat data (messages, logs, stats) for a workflow with timestamp-based selective data transfer.
- Returns all data types in chronological order based on _createdAt timestamp.
- """
- try:
- # Get service center
- interfaceDbChat = _getServiceChat(context)
-
- # Verify workflow exists
- workflow = interfaceDbChat.getWorkflow(workflowId)
- if not workflow:
- raise HTTPException(
- status_code=404,
- detail=f"Workflow with ID {workflowId} not found"
- )
-
- # Get unified chat data using the new method
- chatData = interfaceDbChat.getUnifiedChatData(workflowId, afterTimestamp)
-
- return chatData
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"Error getting unified chat data: {str(e)}", exc_info=True)
- raise HTTPException(
- status_code=500,
- detail=f"Error getting unified chat data: {str(e)}"
- )
diff --git a/modules/routes/routeDataConnections.py b/modules/routes/routeDataConnections.py
index 5d84efd9..95bbd014 100644
--- a/modules/routes/routeDataConnections.py
+++ b/modules/routes/routeDataConnections.py
@@ -43,30 +43,14 @@ def getTokenStatusForConnection(interface, connectionId: str) -> tuple[str, Opti
- tokenExpiresAt: UTC timestamp or None
"""
try:
- # Query tokens table for the latest token for this connection
- tokens = interface.db.getRecordset(
- Token,
- recordFilter={"connectionId": connectionId}
- )
-
- if not tokens:
- return "none", None
-
- # Find the most recent token (highest createdAt timestamp)
- latestToken = None
- latestCreatedAt = 0
-
- for tokenData in tokens:
- createdAt = parseTimestamp(tokenData.get("createdAt"), default=0)
- if createdAt > latestCreatedAt:
- latestCreatedAt = createdAt
- latestToken = tokenData
+ # Query tokens table for the latest token for this connection using interface method
+ latestToken = interface.getConnectionToken(connectionId)
if not latestToken:
return "none", None
# Check if token is expired
- expiresAt = parseTimestamp(latestToken.get("expiresAt"))
+ expiresAt = parseTimestamp(latestToken.expiresAt)
if not expiresAt:
return "none", None
diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py
index 23358947..38877a9f 100644
--- a/modules/routes/routeDataMandates.py
+++ b/modules/routes/routeDataMandates.py
@@ -291,9 +291,9 @@ async def delete_mandate(
)
# MULTI-TENANT: Delete all UserMandate entries for this mandate first
- userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
+ userMandates = appInterface.getUserMandatesByMandate(mandateId)
for um in userMandates:
- appInterface.db.deleteRecord(UserMandate, um["id"])
+ appInterface.deleteUserMandate(str(um.userId), mandateId)
logger.info(f"Deleted {len(userMandates)} UserMandate entries for mandate {mandateId}")
# Delete mandate
@@ -377,39 +377,46 @@ async def list_mandate_users(
)
# Get all UserMandate entries for this mandate
- userMandates = rootInterface.db.getRecordset(
- UserMandate,
- recordFilter={"mandateId": targetMandateId}
- )
+ userMandates = rootInterface.getUserMandatesByMandate(targetMandateId)
result = []
for um in userMandates:
# Get user info
- user = rootInterface.getUser(um.get("userId"))
+ user = rootInterface.getUser(str(um.userId))
if not user:
continue
# Get roles for this membership
- roleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
+ roleIds = rootInterface.getRoleIdsForUserMandate(str(um.id))
- # Resolve role labels for display
+ # Resolve role labels for display (only mandate-level roles, deduplicated)
roleLabels = []
+ filteredRoleIds = []
+ seenLabels = set()
for roleId in roleIds:
role = rootInterface.getRole(roleId)
if role:
- roleLabels.append(role.roleLabel)
+ # Skip feature-instance roles - they don't belong in mandate membership
+ if role.featureInstanceId:
+ continue
+ filteredRoleIds.append(roleId)
+ if role.roleLabel not in seenLabels:
+ roleLabels.append(role.roleLabel)
+ seenLabels.add(role.roleLabel)
else:
- roleLabels.append(roleId) # Fallback to ID if not found
+ # Role not found - fail-safe: skip (no access)
+ logger.warning(f"Role {roleId} not found, skipping")
+ continue
result.append({
- "id": um.get("id"), # UserMandate ID as primary key
+ "id": str(um.id), # UserMandate ID as primary key
"userId": str(user.id),
"username": user.username,
"email": user.email,
"fullName": user.fullName,
- "roleIds": roleIds,
+ "roleIds": filteredRoleIds,
"roleLabels": roleLabels,
- "enabled": um.get("enabled", True)
+ "enabled": um.enabled
})
# Apply search, filtering, and sorting if pagination requested
@@ -545,18 +552,12 @@ async def add_user_to_mandate(
# 6. Validate roles (must exist and belong to this mandate or be global)
for roleId in data.roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roleRecords:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Role {roleId} not found"
- )
- role = roleRecords[0]
- roleMandateId = role.get("mandateId")
- if roleMandateId and str(roleMandateId) != str(targetMandateId):
+ try:
+ rootInterface.validateRoleForMandate(roleId, targetMandateId)
+ except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail=f"Role {roleId} belongs to a different mandate"
+ detail=str(e)
)
# 7. Create UserMandate
@@ -718,18 +719,12 @@ async def update_user_roles_in_mandate(
# Validate new roles
for roleId in roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roleRecords:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Role {roleId} not found"
- )
- role = roleRecords[0]
- roleMandateId = role.get("mandateId")
- if roleMandateId and str(roleMandateId) != str(targetMandateId):
+ try:
+ rootInterface.validateRoleForMandate(roleId, targetMandateId)
+ except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail=f"Role {roleId} belongs to a different mandate"
+ detail=str(e)
)
# Check if removing admin role would leave mandate without admins
@@ -745,12 +740,7 @@ async def update_user_roles_in_mandate(
)
# Remove existing role assignments
- existingRoles = rootInterface.db.getRecordset(
- UserMandateRole,
- recordFilter={"userMandateId": str(membership.id)}
- )
- for er in existingRoles:
- rootInterface.db.recordDelete(UserMandateRole, er.get("id"))
+ rootInterface.deleteUserMandateRoles(str(membership.id))
# Add new role assignments
for roleId in roleIds:
@@ -812,19 +802,17 @@ def _hasMandateAdminRole(context: RequestContext, mandateId: str) -> bool:
rootInterface = interfaceDbApp.getRootInterface()
for roleId in context.roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- roleLabel = role.get("roleLabel", "")
+ role = rootInterface.getRole(roleId)
+ if role:
# Admin role at mandate level (not feature-instance level)
- if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
+ if role.roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
except Exception as e:
logger.error(f"Error checking mandate admin role: {e}")
- return False
+ return False # Fail-safe: no access on error
def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
@@ -832,19 +820,17 @@ def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
Check if excluding this user would leave the mandate without any admins.
"""
try:
- # Get all UserMandates for this mandate
- userMandates = interface.db.getRecordset(
- UserMandate,
- recordFilter={"mandateId": mandateId, "enabled": True}
- )
+ # Get all UserMandates for this mandate (Pydantic models)
+ allMandates = interface.getUserMandatesByMandate(mandateId)
+ userMandates = [um for um in allMandates if um.enabled]
adminCount = 0
for um in userMandates:
- if str(um.get("userId")) == str(excludeUserId):
+ if str(um.userId) == str(excludeUserId):
continue
# Check if this user has admin role
- roleIds = interface.getRoleIdsForUserMandate(um.get("id"))
+ roleIds = interface.getRoleIdsForUserMandate(str(um.id))
if _hasAdminRoleInList(interface, roleIds, mandateId):
adminCount += 1
@@ -852,7 +838,7 @@ def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
except Exception as e:
logger.error(f"Error checking last admin: {e}")
- return True # Fail-safe: assume they're the last admin
+ return True # Fail-safe: assume they're the last admin (prevents deletion)
def _hasAdminRoleInList(interface, roleIds: List[str], mandateId: str) -> bool:
@@ -860,13 +846,10 @@ def _hasAdminRoleInList(interface, roleIds: List[str], mandateId: str) -> bool:
Check if any of the role IDs is an admin role for the mandate.
"""
for roleId in roleIds:
- roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- roleLabel = role.get("roleLabel", "")
- roleMandateId = role.get("mandateId")
- # Admin role at mandate level
- if roleLabel == "admin" and (not roleMandateId or str(roleMandateId) == str(mandateId)):
- if not role.get("featureInstanceId"):
+ role = interface.getRole(roleId)
+ if role:
+ # Admin role at mandate level (global or mandate-specific, not feature-instance)
+ if role.roleLabel == "admin" and not role.featureInstanceId:
+ if not role.mandateId or str(role.mandateId) == str(mandateId):
return True
return False
diff --git a/modules/routes/routeDataUsers.py b/modules/routes/routeDataUsers.py
index f963a33c..5e78d12a 100644
--- a/modules/routes/routeDataUsers.py
+++ b/modules/routes/routeDataUsers.py
@@ -21,7 +21,8 @@ import modules.interfaces.interfaceDbApp as interfaceDbApp
from modules.auth import limiter, getRequestContext, RequestContext
# Import the attribute definition and helper functions
-from modules.datamodels.datamodelUam import User, UserInDB
+from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
+from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
# Configure logger
@@ -251,16 +252,10 @@ async def get_users(
)
elif context.isSysAdmin:
# SysAdmin without mandateId sees all users
- # Get all users directly from database using UserInDB (the actual database model)
- allUsers = appInterface.db.getRecordset(UserInDB)
- # Convert to cleaned dictionaries first for filtering
- cleanedUsers = []
- for u in allUsers:
- cleanedUser = {k: v for k, v in u.items() if not k.startswith("_") and k != "hashedPassword" and k != "resetToken" and k != "resetTokenExpires"}
- # Ensure roleLabels is always a list
- if cleanedUser.get("roleLabels") is None:
- cleanedUser["roleLabels"] = []
- cleanedUsers.append(cleanedUser)
+ # Get all users via interface method (returns Pydantic User models)
+ allUserModels = appInterface.getAllUsers()
+ # Convert to dictionaries for filtering/sorting
+ cleanedUsers = [u.model_dump() for u in allUserModels]
# Apply server-side filtering and sorting
filteredUsers = _applyFiltersAndSort(cleanedUsers, paginationParams)
@@ -331,11 +326,7 @@ async def get_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
- from modules.datamodels.datamodelMembership import UserMandate
- userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
- "userId": userId,
- "mandateId": str(context.mandateId)
- })
+ userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -427,11 +418,7 @@ async def update_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
- from modules.datamodels.datamodelMembership import UserMandate
- userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
- "userId": userId,
- "mandateId": str(context.mandateId)
- })
+ userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -482,11 +469,7 @@ async def reset_user_password(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
- from modules.datamodels.datamodelMembership import UserMandate
- userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
- "userId": userId,
- "mandateId": str(context.mandateId)
- })
+ userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -664,11 +647,7 @@ async def send_password_link(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
- from modules.datamodels.datamodelMembership import UserMandate
- userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
- "userId": userId,
- "mandateId": str(context.mandateId)
- })
+ userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -791,11 +770,7 @@ async def delete_user(
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
if context.mandateId and not context.isSysAdmin:
- from modules.datamodels.datamodelMembership import UserMandate
- userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
- "userId": userId,
- "mandateId": str(context.mandateId)
- })
+ userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -803,10 +778,9 @@ async def delete_user(
)
# Delete UserMandate entries for this user first
- from modules.datamodels.datamodelMembership import UserMandate
- userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
+ userMandates = appInterface.getUserMandates(userId)
for um in userMandates:
- appInterface.db.deleteRecord(UserMandate, um["id"])
+ appInterface.deleteUserMandate(userId, str(um.mandateId))
success = appInterface.deleteUser(userId)
if not success:
diff --git a/modules/routes/routeDataWorkflows.py b/modules/routes/routeDataWorkflows.py
index 799a9855..80ca5986 100644
--- a/modules/routes/routeDataWorkflows.py
+++ b/modules/routes/routeDataWorkflows.py
@@ -163,16 +163,14 @@ async def update_workflow(
# Get workflow interface with current user context
workflowInterface = getInterface(currentUser)
- # Get raw workflow data from database to check permissions
- workflows = workflowInterface.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
- if not workflows:
+ # Get workflow using interface method to check permissions
+ workflow = workflowInterface.getWorkflow(workflowId)
+ if not workflow:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Workflow not found"
)
- workflow_data = workflows[0]
-
# Check if user has permission to update using RBAC
if not workflowInterface.checkRbacPermission(ChatWorkflow, "update", workflowId):
raise HTTPException(
@@ -230,6 +228,49 @@ async def get_workflow_status(
detail=f"Error getting workflow status: {str(e)}"
)
+
+# API Endpoint for stopping a workflow
+@router.post("/{workflowId}/stop", response_model=ChatWorkflow)
+@limiter.limit("120/minute")
+async def stop_workflow(
+ request: Request,
+ workflowId: str = Path(..., description="ID of the workflow to stop"),
+ currentUser: User = Depends(getCurrentUser)
+) -> ChatWorkflow:
+ """
+ Stop a running workflow.
+ This is a general endpoint that can be used by any feature to stop a workflow.
+ """
+ try:
+ from modules.workflows.automation import chatStop
+
+ # Get the workflow first to get mandateId
+ interfaceChatDb = getServiceChat(currentUser)
+ workflow = interfaceChatDb.getWorkflow(workflowId)
+
+ if not workflow:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"Workflow with ID {workflowId} not found"
+ )
+
+ mandateId = workflow.get("mandateId") if isinstance(workflow, dict) else getattr(workflow, "mandateId", None)
+
+ # Stop the workflow
+ stoppedWorkflow = await chatStop(currentUser, workflowId, mandateId=mandateId)
+
+ return stoppedWorkflow
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error stopping workflow: {str(e)}", exc_info=True)
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail=f"Error stopping workflow: {str(e)}"
+ )
+
+
# API Endpoint for workflow logs with selective data transfer
@router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
@limiter.limit("120/minute")
diff --git a/modules/routes/routeGdpr.py b/modules/routes/routeGdpr.py
index 3f06810f..af0c7199 100644
--- a/modules/routes/routeGdpr.py
+++ b/modules/routes/routeGdpr.py
@@ -109,96 +109,73 @@ async def export_user_data(
"authenticationAuthority": str(getattr(currentUser, "authenticationAuthority", ""))
}
- # Mandate memberships
- from modules.datamodels.datamodelMembership import UserMandate
- userMandates = rootInterface.db.getRecordset(
- UserMandate,
- recordFilter={"userId": str(currentUser.id)}
- )
+ # Mandate memberships using interface method
+ userMandates = rootInterface.getUserMandates(str(currentUser.id))
mandates = []
for um in userMandates:
- mandateId = um.get("mandateId")
+ mandateId = um.mandateId
- # Get mandate details
- mandateRecords = rootInterface.db.getRecordset(
- Mandate,
- recordFilter={"id": mandateId}
- )
- mandateName = mandateRecords[0].get("name") if mandateRecords else "Unknown"
+ # Get mandate details using interface method
+ mandate = rootInterface.getMandate(mandateId)
+ mandateName = mandate.name if mandate else "Unknown"
# Get roles for this membership
- roleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
+ roleIds = rootInterface.getRoleIdsForUserMandate(um.id)
mandates.append({
- "userMandateId": um.get("id"),
+ "userMandateId": um.id,
"mandateId": mandateId,
"mandateName": mandateName,
- "enabled": um.get("enabled", True),
+ "enabled": um.enabled,
"roleIds": roleIds,
- "joinedAt": um.get("createdAt")
+ "joinedAt": um.createdAt
})
- # Feature access records
- from modules.datamodels.datamodelMembership import FeatureAccess
- featureAccesses = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": str(currentUser.id)}
- )
+ # Feature access records using interface method
+ featureAccesses = rootInterface.getFeatureAccessesForUser(str(currentUser.id))
featureAccessList = []
for fa in featureAccesses:
- instanceId = fa.get("featureInstanceId")
+ instanceId = fa.featureInstanceId
- # Get instance details
- from modules.datamodels.datamodelFeatures import FeatureInstance
- instanceRecords = rootInterface.db.getRecordset(
- FeatureInstance,
- recordFilter={"id": instanceId}
- )
+ # Get instance details using interface method
+ instance = rootInterface.getFeatureInstance(instanceId)
- instanceInfo = instanceRecords[0] if instanceRecords else {}
- roleIds = rootInterface.getRoleIdsForFeatureAccess(fa.get("id"))
+ roleIds = rootInterface.getRoleIdsForFeatureAccess(fa.id)
featureAccessList.append({
- "featureAccessId": fa.get("id"),
+ "featureAccessId": fa.id,
"featureInstanceId": instanceId,
- "featureCode": instanceInfo.get("featureCode"),
- "instanceLabel": instanceInfo.get("label"),
- "enabled": fa.get("enabled", True),
+ "featureCode": instance.featureCode if instance else None,
+ "instanceLabel": instance.label if instance else None,
+ "enabled": fa.enabled,
"roleIds": roleIds
})
- # Invitations created by user
- from modules.datamodels.datamodelInvitation import Invitation
- invitationsCreated = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"createdBy": str(currentUser.id)}
- )
+ # Invitations created by user using interface method
+ invitationsCreated = rootInterface.getInvitationsByCreator(str(currentUser.id))
invitationsCreatedList = [
{
- "id": inv.get("id"),
- "mandateId": inv.get("mandateId"),
- "createdAt": inv.get("createdAt"),
- "expiresAt": inv.get("expiresAt"),
- "maxUses": inv.get("maxUses"),
- "currentUses": inv.get("currentUses")
+ "id": inv.id,
+ "mandateId": inv.mandateId,
+ "createdAt": inv.createdAt,
+ "expiresAt": inv.expiresAt,
+ "maxUses": inv.maxUses,
+ "currentUses": inv.currentUses
}
for inv in invitationsCreated
]
- # Invitations used by user
- invitationsUsed = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"usedBy": str(currentUser.id)}
- )
+ # Invitations used by user using interface method
+ invitationsUsed = rootInterface.getInvitationsByUsedBy(str(currentUser.id))
invitationsUsedList = [
{
- "id": inv.get("id"),
- "mandateId": inv.get("mandateId"),
- "usedAt": inv.get("usedAt")
+ "id": inv.id,
+ "mandateId": inv.mandateId,
+ "usedAt": inv.usedAt
}
for inv in invitationsUsed
]
@@ -262,26 +239,18 @@ async def export_portable_data(
"additionalProperty": []
}
- # Add mandate memberships as organization affiliations
- from modules.datamodels.datamodelMembership import UserMandate
- userMandates = rootInterface.db.getRecordset(
- UserMandate,
- recordFilter={"userId": str(currentUser.id)}
- )
+ # Add mandate memberships as organization affiliations using interface method
+ userMandates = rootInterface.getUserMandates(str(currentUser.id))
affiliations = []
for um in userMandates:
- mandateRecords = rootInterface.db.getRecordset(
- Mandate,
- recordFilter={"id": um.get("mandateId")}
- )
- if mandateRecords:
- mandate = mandateRecords[0]
+ mandate = rootInterface.getMandate(um.mandateId)
+ if mandate:
affiliations.append({
"@type": "Organization",
- "identifier": um.get("mandateId"),
- "name": mandate.get("name"),
- "membershipActive": um.get("enabled", True)
+ "identifier": um.mandateId,
+ "name": mandate.name,
+ "membershipActive": um.enabled
})
if affiliations:
@@ -370,15 +339,12 @@ async def delete_account(
# Step 2: Revoke invitations BEFORE generic deletion (business logic)
rootInterface = getRootInterface()
from modules.datamodels.datamodelInvitation import Invitation
- userInvitations = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"createdBy": str(currentUser.id)}
- )
+ userInvitations = rootInterface.getInvitationsByCreator(str(currentUser.id))
for inv in userInvitations:
rootInterface.db.recordModify(
Invitation,
- inv.get("id"),
+ inv.id,
{"revokedAt": getUtcTimestamp()}
)
diff --git a/modules/routes/routeInvitations.py b/modules/routes/routeInvitations.py
index 2196bd73..6a53fb38 100644
--- a/modules/routes/routeInvitations.py
+++ b/modules/routes/routeInvitations.py
@@ -131,17 +131,14 @@ async def create_invitation(
# Validate role IDs exist and belong to this mandate or are global
for roleId in data.roleIds:
- from modules.datamodels.datamodelRbac import Role
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if not roleRecords:
+ role = rootInterface.getRole(roleId)
+ if not role:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Role '{roleId}' not found"
)
- role = roleRecords[0]
# Role must be global or belong to this mandate
- roleMandateId = role.get("mandateId")
- if roleMandateId and str(roleMandateId) != str(context.mandateId):
+ if role.mandateId and str(role.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Role '{roleId}' belongs to a different mandate"
@@ -149,18 +146,13 @@ async def create_invitation(
# Validate feature instance if provided
if data.featureInstanceId:
- from modules.datamodels.datamodelFeatures import FeatureInstance
- instanceRecords = rootInterface.db.getRecordset(
- FeatureInstance,
- recordFilter={"id": data.featureInstanceId}
- )
- if not instanceRecords:
+ instance = rootInterface.getFeatureInstance(data.featureInstanceId)
+ if not instance:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Feature instance '{data.featureInstanceId}' not found"
)
- instance = instanceRecords[0]
- if str(instance.get("mandateId")) != str(context.mandateId):
+ if str(instance.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Feature instance belongs to a different mandate"
@@ -196,14 +188,9 @@ async def create_invitation(
if data.email:
try:
from modules.connectors.connectorMessagingEmail import ConnectorMessagingEmail
- from modules.datamodels.datamodelUam import Mandate
-
# Get mandate name for the email
- mandateRecords = rootInterface.db.getRecordset(
- Mandate,
- recordFilter={"id": str(context.mandateId)}
- )
- mandateName = mandateRecords[0].get("name", "PowerOn") if mandateRecords else "PowerOn"
+ mandate = rootInterface.getMandate(str(context.mandateId))
+ mandateName = mandate.name if mandate else "PowerOn"
emailConnector = ConnectorMessagingEmail()
emailSubject = f"Einladung zu {mandateName}"
@@ -259,14 +246,10 @@ async def create_invitation(
existingUser = rootInterface.getUserByUsername(data.targetUsername)
if existingUser:
from modules.routes.routeNotifications import createInvitationNotification
- from modules.datamodels.datamodelUam import Mandate
# Get mandate name for notification
- mandateRecords = rootInterface.db.getRecordset(
- Mandate,
- recordFilter={"id": str(context.mandateId)}
- )
- mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
+ mandate = rootInterface.getMandate(str(context.mandateId))
+ mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else "PowerOn"
inviterName = context.user.fullName or context.user.username
createInvitationNotification(
@@ -348,38 +331,38 @@ async def list_invitations(
try:
rootInterface = getRootInterface()
- # Get all invitations for this mandate
- allInvitations = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"mandateId": str(context.mandateId)}
- )
+ # Get all invitations for this mandate (Pydantic models)
+ allInvitations = rootInterface.getInvitationsByMandate(str(context.mandateId))
currentTime = getUtcTimestamp()
result = []
for inv in allInvitations:
# Skip revoked invitations
- if inv.get("revokedAt"):
+ if inv.revokedAt:
continue
# Filter by usage
- if not includeUsed and inv.get("currentUses", 0) >= inv.get("maxUses", 1):
+ currentUses = inv.currentUses or 0
+ maxUses = inv.maxUses or 1
+ if not includeUsed and currentUses >= maxUses:
continue
# Filter by expiration
- if not includeExpired and inv.get("expiresAt", 0) < currentTime:
+ expiresAt = inv.expiresAt or 0
+ if not includeExpired and expiresAt < currentTime:
continue
# Build invite URL
from modules.shared.configuration import APP_CONFIG
frontendUrl = APP_CONFIG.get("APP_FRONTEND_URL", "http://localhost:8080")
- inviteUrl = f"{frontendUrl}/invite/{inv.get('token')}"
+ inviteUrl = f"{frontendUrl}/invite/{inv.token}"
result.append({
- **{k: v for k, v in inv.items() if not k.startswith("_")},
+ **inv.model_dump(),
"inviteUrl": inviteUrl,
- "isExpired": inv.get("expiresAt", 0) < currentTime,
- "isUsedUp": inv.get("currentUses", 0) >= inv.get("maxUses", 1)
+ "isExpired": expiresAt < currentTime,
+ "isUsedUp": currentUses >= maxUses
})
return result
@@ -425,29 +408,24 @@ async def revoke_invitation(
try:
rootInterface = getRootInterface()
- # Get invitation
- invitationRecords = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"id": invitationId}
- )
+ # Get invitation (Pydantic model)
+ invitation = rootInterface.getInvitation(invitationId)
- if not invitationRecords:
+ if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Invitation '{invitationId}' not found"
)
- invitation = invitationRecords[0]
-
# Verify mandate access
- if str(invitation.get("mandateId")) != str(context.mandateId):
+ if str(invitation.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this invitation"
)
# Already revoked?
- if invitation.get("revokedAt"):
+ if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation is already revoked"
@@ -496,13 +474,10 @@ async def validate_invitation(
try:
rootInterface = getRootInterface()
- # Find invitation by token
- invitationRecords = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"token": token}
- )
+ # Find invitation by token (Pydantic model)
+ invitation = rootInterface.getInvitationByToken(token)
- if not invitationRecords:
+ if not invitation:
return InvitationValidation(
valid=False,
reason="Invitation not found",
@@ -511,10 +486,8 @@ async def validate_invitation(
roleIds=[]
)
- invitation = invitationRecords[0]
-
# Check if revoked
- if invitation.get("revokedAt"):
+ if invitation.revokedAt:
return InvitationValidation(
valid=False,
reason="Invitation has been revoked",
@@ -525,7 +498,8 @@ async def validate_invitation(
# Check if expired
currentTime = getUtcTimestamp()
- if invitation.get("expiresAt", 0) < currentTime:
+ expiresAt = invitation.expiresAt or 0
+ if expiresAt < currentTime:
return InvitationValidation(
valid=False,
reason="Invitation has expired",
@@ -535,7 +509,9 @@ async def validate_invitation(
)
# Check if used up
- if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
+ currentUses = invitation.currentUses or 0
+ maxUses = invitation.maxUses or 1
+ if currentUses >= maxUses:
return InvitationValidation(
valid=False,
reason="Invitation has reached maximum uses",
@@ -545,34 +521,29 @@ async def validate_invitation(
)
# Get additional info for display
- mandateId = invitation.get("mandateId")
+ mandateId = invitation.mandateId
mandateName = None
roleLabels = []
- targetUsername = invitation.get("targetUsername")
+ targetUsername = invitation.targetUsername
# Get mandate name
- from modules.datamodels.datamodelUam import Mandate
- mandateRecords = rootInterface.db.getRecordset(
- Mandate,
- recordFilter={"id": mandateId}
- )
- if mandateRecords:
- mandateName = mandateRecords[0].get("name")
+ mandate = rootInterface.getMandate(str(mandateId)) if mandateId else None
+ if mandate:
+ mandateName = mandate.name
# Get role names
- roleIds = invitation.get("roleIds", [])
- from modules.datamodels.datamodelRbac import Role
+ roleIds = invitation.roleIds or []
for roleId in roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- roleLabels.append(roleRecords[0].get("roleLabel", roleId))
+ role = rootInterface.getRole(roleId)
+ if role:
+ roleLabels.append(role.roleLabel)
return InvitationValidation(
valid=True,
reason=None,
- mandateId=mandateId,
+ mandateId=str(mandateId) if mandateId else None,
mandateName=mandateName,
- featureInstanceId=invitation.get("featureInstanceId"),
+ featureInstanceId=str(invitation.featureInstanceId) if invitation.featureInstanceId else None,
roleIds=roleIds,
roleLabels=roleLabels,
targetUsername=targetUsername
@@ -608,42 +579,40 @@ async def accept_invitation(
try:
rootInterface = getRootInterface()
- # Find invitation by token
- invitationRecords = rootInterface.db.getRecordset(
- Invitation,
- recordFilter={"token": token}
- )
+ # Find invitation by token (Pydantic model)
+ invitation = rootInterface.getInvitationByToken(token)
- if not invitationRecords:
+ if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invitation not found"
)
- invitation = invitationRecords[0]
-
# Validate invitation
- if invitation.get("revokedAt"):
+ if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has been revoked"
)
currentTime = getUtcTimestamp()
- if invitation.get("expiresAt", 0) < currentTime:
+ expiresAt = invitation.expiresAt or 0
+ if expiresAt < currentTime:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has expired"
)
- if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
+ currentUses = invitation.currentUses or 0
+ maxUses = invitation.maxUses or 1
+ if currentUses >= maxUses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has reached maximum uses"
)
# Validate username matches - the invitation is bound to a specific user
- targetUsername = invitation.get("targetUsername")
+ targetUsername = invitation.targetUsername
if targetUsername and currentUser.username != targetUsername:
logger.warning(
f"User {currentUser.username} tried to accept invitation meant for {targetUsername}"
@@ -653,9 +622,9 @@ async def accept_invitation(
detail=f"Diese Einladung ist für Benutzer '{targetUsername}' bestimmt"
)
- mandateId = invitation.get("mandateId")
- roleIds = invitation.get("roleIds", [])
- featureInstanceId = invitation.get("featureInstanceId")
+ mandateId = str(invitation.mandateId) if invitation.mandateId else None
+ roleIds = invitation.roleIds or []
+ featureInstanceId = str(invitation.featureInstanceId) if invitation.featureInstanceId else None
# Check if user is already a member
existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId)
@@ -744,22 +713,19 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
try:
rootInterface = getRootInterface()
- from modules.datamodels.datamodelRbac import Role
for roleId in context.roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- roleLabel = role.get("roleLabel", "")
- # Admin role at mandate level
- if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
+ role = rootInterface.getRole(roleId)
+ if role:
+ # Admin role at mandate level (not feature-instance level)
+ if role.roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
return True
return False
except Exception as e:
logger.error(f"Error checking mandate admin role: {e}")
- return False
+ return False # Fail-safe: no access on error
def _isInstanceRole(interface, roleId: str, featureInstanceId: str) -> bool:
@@ -767,11 +733,9 @@ def _isInstanceRole(interface, roleId: str, featureInstanceId: str) -> bool:
Check if a role belongs to a specific feature instance.
"""
try:
- from modules.datamodels.datamodelRbac import Role
- roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- return str(role.get("featureInstanceId", "")) == str(featureInstanceId)
+ role = interface.getRole(roleId)
+ if role:
+ return str(role.featureInstanceId or "") == str(featureInstanceId)
return False
except Exception:
- return False
+ return False # Fail-safe: assume not instance role on error
diff --git a/modules/routes/routeMessaging.py b/modules/routes/routeMessaging.py
index 753fb16f..419e9ae6 100644
--- a/modules/routes/routeMessaging.py
+++ b/modules/routes/routeMessaging.py
@@ -421,10 +421,9 @@ def _hasTriggerPermission(context: RequestContext) -> bool:
rootInterface = getRootInterface()
for roleId in context.roleIds:
- roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roleRecords:
- role = roleRecords[0]
- roleLabel = role.get("roleLabel", "")
+ role = rootInterface.getRole(roleId)
+ if role:
+ roleLabel = role.roleLabel
# Admin role at mandate level or system admin
if roleLabel in ("admin", "sysadmin"):
return True
diff --git a/modules/routes/routeNotifications.py b/modules/routes/routeNotifications.py
index 7c8cf9ad..4fc09ac4 100644
--- a/modules/routes/routeNotifications.py
+++ b/modules/routes/routeNotifications.py
@@ -137,23 +137,19 @@ async def getNotifications(
# Build filter
recordFilter = {"userId": str(currentUser.id)}
- if status:
- recordFilter["status"] = status
- if type:
- recordFilter["type"] = type
-
- # Get notifications
- notifications = rootInterface.db.getRecordset(
- model_class=UserNotification,
- recordFilter=recordFilter
+ # Get notifications (Pydantic models, sorted and limited)
+ notifications = rootInterface.getNotificationsByUser(
+ userId=str(currentUser.id),
+ status=status,
+ limit=limit
)
- # Sort by creation date (newest first) and limit
- notifications = sorted(notifications, key=lambda x: x.get("createdAt", 0), reverse=True)
- if limit:
- notifications = notifications[:limit]
+ # Apply type filter if needed (not common, so filter post-fetch)
+ if type:
+ notifications = [n for n in notifications if n.type == type]
- return notifications
+ # Convert to dicts for response
+ return [n.model_dump() for n in notifications]
except Exception as e:
logger.error(f"Error getting notifications: {e}")
@@ -176,12 +172,10 @@ async def getUnreadCount(
try:
rootInterface = getRootInterface()
- notifications = rootInterface.db.getRecordset(
- model_class=UserNotification,
- recordFilter={
- "userId": str(currentUser.id),
- "status": NotificationStatus.UNREAD.value
- }
+ # Get unread notifications (Pydantic models)
+ notifications = rootInterface.getNotificationsByUser(
+ userId=str(currentUser.id),
+ status=NotificationStatus.UNREAD.value
)
return UnreadCountResponse(count=len(notifications))
@@ -207,22 +201,17 @@ async def markAsRead(
try:
rootInterface = getRootInterface()
- # Get the notification
- notifications = rootInterface.db.getRecordset(
- model_class=UserNotification,
- recordFilter={"id": notificationId}
- )
+ # Get the notification (Pydantic model)
+ notification = rootInterface.getNotification(notificationId)
- if not notifications:
+ if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
- notification = notifications[0]
-
# Verify ownership
- if notification.get("userId") != currentUser.id:
+ if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this notification"
@@ -262,13 +251,10 @@ async def markAllAsRead(
try:
rootInterface = getRootInterface()
- # Get all unread notifications
- notifications = rootInterface.db.getRecordset(
- model_class=UserNotification,
- recordFilter={
- "userId": currentUser.id,
- "status": NotificationStatus.UNREAD.value
- }
+ # Get all unread notifications (Pydantic models)
+ notifications = rootInterface.getNotificationsByUser(
+ userId=str(currentUser.id),
+ status=NotificationStatus.UNREAD.value
)
currentTime = getUtcTimestamp()
@@ -277,7 +263,7 @@ async def markAllAsRead(
for notification in notifications:
rootInterface.db.recordModify(
model_class=UserNotification,
- recordId=notification.get("id"),
+ recordId=str(notification.id),
record={
"status": NotificationStatus.READ.value,
"readAt": currentTime
@@ -309,37 +295,32 @@ async def executeAction(
try:
rootInterface = getRootInterface()
- # Get the notification
- notifications = rootInterface.db.getRecordset(
- model_class=UserNotification,
- recordFilter={"id": notificationId}
- )
+ # Get the notification (Pydantic model)
+ notification = rootInterface.getNotification(notificationId)
- if not notifications:
+ if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
- notification = notifications[0]
-
# Verify ownership
- if notification.get("userId") != currentUser.id:
+ if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this notification"
)
# Check if already actioned
- if notification.get("status") == NotificationStatus.ACTIONED.value:
+ if notification.status == NotificationStatus.ACTIONED.value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Notification has already been actioned"
)
# Validate action exists
- actions = notification.get("actions", [])
- validActionIds = [a.get("actionId") if isinstance(a, dict) else a.actionId for a in (actions or [])]
+ actions = notification.actions or []
+ validActionIds = [a.get("actionId") if isinstance(a, dict) else a.actionId for a in actions]
if actionRequest.actionId not in validActionIds:
raise HTTPException(
@@ -407,22 +388,17 @@ async def _handleInvitationAction(
detail="No invitation reference found"
)
- # Get the invitation
- invitations = rootInterface.db.getRecordset(
- model_class=Invitation,
- recordFilter={"id": invitationId}
- )
+ # Get the invitation (Pydantic model)
+ invitation = rootInterface.getInvitation(invitationId)
- if not invitations:
+ if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invitation not found"
)
- invitation = invitations[0]
-
# Verify username matches
- if invitation.get("targetUsername") != currentUser.username:
+ if invitation.targetUsername != currentUser.username:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This invitation is for a different user"
@@ -430,19 +406,22 @@ async def _handleInvitationAction(
# Check if invitation is still valid
currentTime = getUtcTimestamp()
- if invitation.get("expiresAt", 0) < currentTime:
+ expiresAt = invitation.expiresAt or 0
+ if expiresAt < currentTime:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has expired"
)
- if invitation.get("revokedAt"):
+ if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has been revoked"
)
- if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
+ currentUses = invitation.currentUses or 0
+ maxUses = invitation.maxUses or 1
+ if currentUses >= maxUses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invitation has reached maximum uses"
@@ -450,59 +429,34 @@ async def _handleInvitationAction(
if actionId == "accept":
# Accept the invitation - assign roles and mandate access
- mandateId = invitation.get("mandateId")
- roleIds = invitation.get("roleIds", [])
+ mandateId = str(invitation.mandateId) if invitation.mandateId else None
+ roleIds = list(invitation.roleIds or [])
# Ensure user gets the system "user" role for access to public UI elements (e.g. playground)
- userRoles = rootInterface.db.getRecordset(
- model_class=Role,
- recordFilter={"roleLabel": "user"}
- )
- if userRoles:
- userRoleId = userRoles[0].get("id")
+ userRole = rootInterface.getRoleByLabel("user")
+ if userRole:
+ userRoleId = str(userRole.id)
if userRoleId and userRoleId not in roleIds:
roleIds = roleIds + [userRoleId]
logger.debug(f"Added system 'user' role {userRoleId} to invitation roles")
# Get mandate name for result message
- mandates = rootInterface.db.getRecordset(
- model_class=Mandate,
- recordFilter={"id": mandateId}
- )
- mandateName = mandates[0].get("mandateLabel", mandateId) if mandates else mandateId
+ mandate = rootInterface.getMandate(mandateId) if mandateId else None
+ mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else mandateId
# Check if user already has this mandate
- existingMemberships = rootInterface.db.getRecordset(
- model_class=UserMandate,
- recordFilter={
- "userId": currentUser.id,
- "mandateId": mandateId
- }
- )
+ existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId) if mandateId else None
- if existingMemberships:
- # Update existing membership with new roles
- existingMembership = existingMemberships[0]
- existingRoles = existingMembership.get("roleIds", [])
- mergedRoles = list(set(existingRoles + roleIds))
-
- rootInterface.db.recordModify(
- model_class=UserMandate,
- recordId=existingMembership.get("id"),
- record={"roleIds": mergedRoles}
- )
- logger.info(f"Updated UserMandate for user {currentUser.id} in mandate {mandateId}")
+ if existingMembership:
+ # Update existing membership with new roles via interface
+ # Note: roleIds on UserMandate is deprecated - roles should be assigned via UserMandateRole
+ logger.info(f"User {currentUser.id} already has membership in mandate {mandateId}, adding roles via UserMandateRole")
+ # Add roles via junction table
+ for roleId in roleIds:
+ rootInterface.addRoleToUserMandate(str(existingMembership.id), roleId)
else:
- # Create new user-mandate relationship
- userMandate = UserMandate(
- userId=currentUser.id,
- mandateId=mandateId,
- roleIds=roleIds
- )
- rootInterface.db.recordCreate(
- model_class=UserMandate,
- record=userMandate.model_dump()
- )
+ # Create new user-mandate relationship via interface
+ rootInterface.createUserMandate(str(currentUser.id), mandateId, roleIds)
logger.info(f"Created UserMandate for user {currentUser.id} in mandate {mandateId}")
# Mark invitation as used
@@ -510,9 +464,9 @@ async def _handleInvitationAction(
model_class=Invitation,
recordId=invitationId,
record={
- "usedBy": currentUser.id,
+ "usedBy": str(currentUser.id),
"usedAt": currentTime,
- "currentUses": invitation.get("currentUses", 0) + 1
+ "currentUses": currentUses + 1
}
)
@@ -545,22 +499,17 @@ async def deleteNotification(
try:
rootInterface = getRootInterface()
- # Get the notification
- notifications = rootInterface.db.getRecordset(
- model_class=UserNotification,
- recordFilter={"id": notificationId}
- )
+ # Get the notification (Pydantic model)
+ notification = rootInterface.getNotification(notificationId)
- if not notifications:
+ if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification not found"
)
- notification = notifications[0]
-
# Verify ownership
- if notification.get("userId") != currentUser.id:
+ if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this notification"
diff --git a/modules/routes/routeSecurityAdmin.py b/modules/routes/routeSecurityAdmin.py
index 36388c9f..75490eac 100644
--- a/modules/routes/routeSecurityAdmin.py
+++ b/modules/routes/routeSecurityAdmin.py
@@ -125,8 +125,8 @@ async def list_tokens(
if statusFilter:
recordFilter["status"] = statusFilter
# MULTI-TENANT: SysAdmin sees ALL tokens (no mandate filter)
-
- tokens = appInterface.db.getRecordset(Token, recordFilter=recordFilter)
+ # Use interface method to get tokens with flexible filtering
+ tokens = appInterface.getAllTokens(recordFilter=recordFilter)
return tokens
except HTTPException:
raise
@@ -254,15 +254,13 @@ async def revoke_tokens_by_mandate(
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
appInterface = getRootInterface()
- # Get all UserMandate entries for this mandate to find users
- # Note: In new model, users are linked via UserMandate, not User.mandateId
- from modules.datamodels.datamodelMembership import UserMandate
- userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
+ # Get all UserMandate entries for this mandate to find users using interface method
+ userMandates = appInterface.getUserMandatesByMandate(mandateId)
total = 0
for um in userMandates:
total += appInterface.revokeTokensByUser(
- userId=um["userId"],
+ userId=um.userId,
authority=AuthAuthority(authority) if authority else None,
mandateId=None, # Revoke all tokens for user
revokedBy=currentUser.id,
diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py
index a4795243..d8ef3bef 100644
--- a/modules/routes/routeSecurityGoogle.py
+++ b/modules/routes/routeSecurityGoogle.py
@@ -15,7 +15,7 @@ import httpx
from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
-from modules.auth import getCurrentUser, limiter
+from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.auth import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
from modules.auth.tokenManager import TokenManager
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
@@ -171,10 +171,9 @@ async def login(
try:
if connectionId:
rootInterface = getRootInterface()
- records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
- if records:
- record = records[0]
- login_hint = record.get("externalEmail") or record.get("externalUsername")
+ connection = rootInterface.getUserConnectionById(connectionId)
+ if connection:
+ login_hint = connection.externalEmail or connection.externalUsername
if login_hint:
extra_params["login_hint"] = login_hint
if "@" in login_hint:
@@ -260,23 +259,20 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
rootInterface = getRootInterface()
# Prefer connection flow reuse; fallback to user access token
if connection_id:
- existing_tokens = rootInterface.db.getRecordset(Token, recordFilter={
- "connectionId": connection_id,
- "authority": AuthAuthority.GOOGLE
- })
+ existing_tokens = rootInterface.getTokensByConnectionIdAndAuthority(
+ connection_id, AuthAuthority.GOOGLE
+ )
if existing_tokens:
# Use most recent by createdAt
- existing_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True)
- token_response["refresh_token"] = existing_tokens[0].get("tokenRefresh", "")
+ existing_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
+ token_response["refresh_token"] = existing_tokens[0].tokenRefresh or ""
if not token_response.get("refresh_token") and user_id:
- existing_access_tokens = rootInterface.db.getRecordset(Token, recordFilter={
- "userId": user_id,
- "connectionId": None,
- "authority": AuthAuthority.GOOGLE
- })
+ existing_access_tokens = rootInterface.getTokensByUserIdNoConnection(
+ user_id, AuthAuthority.GOOGLE
+ )
if existing_access_tokens:
- existing_access_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True)
- token_response["refresh_token"] = existing_access_tokens[0].get("tokenRefresh", "")
+ existing_access_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
+ token_response["refresh_token"] = existing_access_tokens[0].tokenRefresh or ""
except Exception:
# Non-fatal; continue without refresh token
pass
diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py
index 8f11a9af..5f132833 100644
--- a/modules/routes/routeSecurityLocal.py
+++ b/modules/routes/routeSecurityLocal.py
@@ -330,40 +330,34 @@ Falls Sie sich nicht registriert haben, können Sie diese E-Mail ignorieren."""
from modules.datamodels.datamodelUam import Mandate
currentTime = getUtcTimestamp()
- pendingInvitations = appInterface.db.getRecordset(
- model_class=Invitation,
- recordFilter={"targetUsername": userData.username}
- )
+ pendingInvitations = appInterface.getInvitationsByTargetUsername(userData.username)
for invitation in pendingInvitations:
# Skip expired, revoked, or fully used invitations
- if invitation.get("expiresAt", 0) < currentTime:
+ if (invitation.expiresAt or 0) < currentTime:
continue
- if invitation.get("revokedAt"):
+ if invitation.revokedAt:
continue
- if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
+ if (invitation.currentUses or 0) >= (invitation.maxUses or 1):
continue
- # Get mandate name for notification
- mandateId = invitation.get("mandateId")
- mandateRecords = appInterface.db.getRecordset(
- Mandate,
- recordFilter={"id": mandateId}
- )
- mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
+ # Get mandate name for notification using interface method
+ mandateId = invitation.mandateId
+ mandate = appInterface.getMandate(mandateId)
+ mandateName = mandate.mandateLabel if mandate else "PowerOn"
# Get inviter name
- inviterId = invitation.get("createdBy")
+ inviterId = invitation.createdBy
inviter = appInterface.getUserById(inviterId) if inviterId else None
inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn"
createInvitationNotification(
userId=str(user.id),
- invitationId=str(invitation.get("id")),
+ invitationId=str(invitation.id),
mandateName=mandateName,
inviterName=inviterName
)
- logger.info(f"Created notification for new user {userData.username} for invitation {invitation.get('id')}")
+ logger.info(f"Created notification for new user {userData.username} for invitation {invitation.id}")
except Exception as notifErr:
logger.warning(f"Failed to create notifications for pending invitations: {notifErr}")
diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py
index 921ddafe..68bf6fe8 100644
--- a/modules/routes/routeSecurityMsft.py
+++ b/modules/routes/routeSecurityMsft.py
@@ -16,7 +16,7 @@ from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.datamodels.datamodelSecurity import Token
-from modules.auth import getCurrentUser, limiter
+from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.auth import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
from modules.auth.tokenManager import TokenManager
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
@@ -97,11 +97,10 @@ async def login(
if connectionId:
try:
rootInterface = getRootInterface()
- # Fetch the connection by ID directly
- records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
- if records:
- record = records[0]
- login_hint = record.get("externalEmail") or record.get("externalUsername")
+ # Fetch the connection by ID directly using interface method
+ connection = rootInterface.getUserConnectionById(connectionId)
+ if connection:
+ login_hint = connection.externalEmail or connection.externalUsername
if login_hint:
login_kwargs["login_hint"] = login_hint
# Derive domain hint from email/UPN
diff --git a/modules/routes/routeSystem.py b/modules/routes/routeSystem.py
index 4e2f9f8f..04e14063 100644
--- a/modules/routes/routeSystem.py
+++ b/modules/routes/routeSystem.py
@@ -38,13 +38,13 @@ def _getUserRoleIds(userId: str) -> List[str]:
rootInterface = getRootInterface()
roleIds = []
- userMandates = rootInterface.db.getRecordset(
- UserMandate,
- recordFilter={"userId": userId, "enabled": True}
- )
+ # Get UserMandates as Pydantic models
+ userMandates = rootInterface.getUserMandates(userId)
for um in userMandates:
- mandateRoleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
+ if not um.enabled:
+ continue
+ mandateRoleIds = rootInterface.getRoleIdsForUserMandate(str(um.id))
for rid in mandateRoleIds:
if rid not in roleIds:
roleIds.append(rid)
@@ -60,30 +60,24 @@ def _checkUiPermission(roleIds: List[str], objectKey: str) -> bool:
rootInterface = getRootInterface()
for roleId in roleIds:
- # Get UI rules for this role
- rules = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": roleId, "context": "UI"}
- )
+ # Get UI rules for this role (returns Pydantic AccessRule models)
+ rules = rootInterface.getAccessRules(roleId=roleId, context=AccessRuleContext.UI)
for rule in rules:
- ruleItem = rule.get("item")
- ruleView = rule.get("view", False)
-
- if not ruleView:
+ if not rule.view:
continue
# Global rule (item=None) grants access to all UI
- if ruleItem is None:
+ if rule.item is None:
return True
# Exact match
- if ruleItem == objectKey:
+ if rule.item == objectKey:
return True
# Wildcard match (e.g., ui.system.* matches ui.system.playground)
- if ruleItem.endswith(".*"):
- prefix = ruleItem[:-2]
+ if rule.item.endswith(".*"):
+ prefix = rule.item[:-2]
if objectKey.startswith(prefix):
return True
@@ -108,6 +102,12 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
elif featureCode == "realestate":
from modules.features.realestate.mainRealEstate import UI_OBJECTS
return UI_OBJECTS
+ elif featureCode == "chatplayground":
+ from modules.features.chatplayground.mainChatplayground import UI_OBJECTS
+ return UI_OBJECTS
+ elif featureCode == "automation":
+ from modules.features.automation.mainAutomation import UI_OBJECTS
+ return UI_OBJECTS
else:
logger.warning(f"Unknown feature code: {featureCode}")
return []
@@ -287,67 +287,50 @@ def _getInstanceViewPermissions(
permissions = {"_all": False, "isAdmin": False}
try:
- from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role
+ # Get FeatureAccess for this user and instance (Pydantic model)
+ featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
- # Get FeatureAccess for this user and instance
- featureAccesses = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": userId, "featureInstanceId": instanceId}
- )
-
- if not featureAccesses:
+ if not featureAccess:
return permissions
- # Get role IDs via FeatureAccessRole junction table
- featureAccessId = featureAccesses[0].get("id")
- featureAccessRoles = rootInterface.db.getRecordset(
- FeatureAccessRole,
- recordFilter={"featureAccessId": featureAccessId}
- )
- roleIds = [far.get("roleId") for far in featureAccessRoles]
+ # Get role IDs via interface method
+ roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
if not roleIds:
return permissions
# Check if user has admin role
for roleId in roleIds:
- roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
- if roles:
- roleLabel = roles[0].get("roleLabel", "").lower()
- if "admin" in roleLabel:
- permissions["isAdmin"] = True
- break
+ role = rootInterface.getRole(roleId)
+ if role and "admin" in role.roleLabel.lower():
+ permissions["isAdmin"] = True
+ break
- # Get UI permissions from AccessRules
- # Permissions are stored with full objectKey (e.g., ui.feature.trustee.dashboard)
+ # Get UI permissions from AccessRules (Pydantic models)
for roleId in roleIds:
- accessRules = rootInterface.db.getRecordset(
- AccessRule,
- recordFilter={"roleId": roleId, "context": "UI"}
- )
+ accessRules = rootInterface.getAccessRules(roleId=roleId, context=AccessRuleContext.UI)
logger.debug(f"_getInstanceViewPermissions: roleId={roleId}, UI rules count={len(accessRules)}")
for rule in accessRules:
- if not rule.get("view", False):
+ if not rule.view:
continue
- item = rule.get("item")
- logger.debug(f"_getInstanceViewPermissions: rule item={item}, view={rule.get('view')}")
+ logger.debug(f"_getInstanceViewPermissions: rule item={rule.item}, view={rule.view}")
- if item is None:
+ if rule.item is None:
# item=None means all views
permissions["_all"] = True
else:
# Store full objectKey as per Navigation-API-Konzept
- permissions[item] = True
+ permissions[rule.item] = True
logger.debug(f"_getInstanceViewPermissions: final permissions={permissions}")
return permissions
except Exception as e:
logger.debug(f"Error getting instance view permissions: {e}")
- return permissions
+ return permissions # Fail-safe: no permissions on error
def _buildStaticBlocks(
diff --git a/modules/security/rbac.py b/modules/security/rbac.py
index 11d6e11d..c661e795 100644
--- a/modules/security/rbac.py
+++ b/modules/security/rbac.py
@@ -173,7 +173,7 @@ class RbacClass:
try:
# Get Root mandate ID (first mandate in system)
allMandates = self.dbApp.getRecordset(Mandate)
- rootMandateId = allMandates[0].get("id") if allMandates else None
+ rootMandateId = allMandates[0]["id"] if allMandates else None
# Collect mandates to check:
# - If mandateId provided: current mandate + Root mandate (if different)
@@ -186,21 +186,21 @@ class RbacClass:
# Load roles from each mandate
for checkMandateId in mandatesToCheck:
- userMandates = self.dbApp.getRecordset(
+ userMandateRecords = self.dbApp.getRecordset(
UserMandate,
recordFilter={"userId": user.id, "mandateId": checkMandateId, "enabled": True}
)
- if userMandates:
- userMandateId = userMandates[0].get("id")
+ if userMandateRecords:
+ userMandateId = userMandateRecords[0]["id"]
# Lade UserMandateRoles (Mandate-level roles)
- userMandateRoles = self.dbApp.getRecordset(
+ userMandateRoleRecords = self.dbApp.getRecordset(
UserMandateRole,
recordFilter={"userMandateId": userMandateId}
)
- foundRoles = [r.get("roleId") for r in userMandateRoles if r.get("roleId")]
+ foundRoles = [r["roleId"] for r in userMandateRoleRecords if r.get("roleId")]
roleIds.update(foundRoles)
# Load FeatureAccess + FeatureAccessRole (Instance-level roles)
@@ -215,14 +215,14 @@ class RbacClass:
)
if featureAccessRecords:
- featureAccessId = featureAccessRecords[0].get("id")
+ featureAccessId = featureAccessRecords[0]["id"]
- featureAccessRoles = self.dbApp.getRecordset(
+ featureAccessRoleRecords = self.dbApp.getRecordset(
FeatureAccessRole,
recordFilter={"featureAccessId": featureAccessId}
)
- roleIds.update([r.get("roleId") for r in featureAccessRoles if r.get("roleId")])
+ roleIds.update([r["roleId"] for r in featureAccessRoleRecords if r.get("roleId")])
except Exception as e:
logger.error(f"Error loading role IDs for user {user.id}: {e}")
@@ -377,12 +377,14 @@ class RbacClass:
if not roleRecords:
continue
- role = roleRecords[0]
+ # Convert to Pydantic model for type-safe access
+ roleDict = {k: v for k, v in roleRecords[0].items() if not k.startswith("_")}
+ role = Role(**roleDict)
# Bestimme Priorität basierend auf Role-Scope
- if role.get("featureInstanceId"):
+ if role.featureInstanceId:
priority = 3 # Instance-specific
- elif role.get("mandateId"):
+ elif role.mandateId:
priority = 2 # Mandate-specific
else:
priority = 1 # Global
diff --git a/modules/services/serviceChat/mainServiceChat.py b/modules/services/serviceChat/mainServiceChat.py
index 137dcd05..b7910720 100644
--- a/modules/services/serviceChat/mainServiceChat.py
+++ b/modules/services/serviceChat/mainServiceChat.py
@@ -681,7 +681,7 @@ class ChatService:
"workflowId": workflow.id,
"process": process,
"engine": aiResponse.modelName,
- "priceUsd": aiResponse.priceUsd,
+ "priceCHF": aiResponse.priceCHF,
"processingTime": aiResponse.processingTime,
"bytesSent": aiResponse.bytesSent,
"bytesReceived": aiResponse.bytesReceived,
diff --git a/modules/services/serviceExtraction/mainServiceExtraction.py b/modules/services/serviceExtraction/mainServiceExtraction.py
index 4081158d..9ee9e739 100644
--- a/modules/services/serviceExtraction/mainServiceExtraction.py
+++ b/modules/services/serviceExtraction/mainServiceExtraction.py
@@ -39,7 +39,7 @@ class ExtractionService:
# Verify required internal model is available (used for pricing in extractContent)
modelDisplayName = "Internal Document Extractor"
model = modelRegistry.getModel(modelDisplayName)
- if model is None or model.calculatePriceUsd is None:
+ if model is None or model.calculatepriceCHF is None:
raise RuntimeError(f"FATAL: Required internal model '{modelDisplayName}' is not available. Check connector registration.")
def extractContent(
@@ -218,18 +218,18 @@ class ExtractionService:
modelDisplayName = "Internal Document Extractor"
model = modelRegistry.getModel(modelDisplayName)
# Hard fail if model is missing; caller must ensure connectors are registered
- if model is None or model.calculatePriceUsd is None:
+ if model is None or model.calculatepriceCHF is None:
if docOperationId:
self.services.chat.progressLogFinish(docOperationId, False)
raise RuntimeError(f"Pricing model not available: {modelDisplayName}")
- priceUsd = model.calculatePriceUsd(processingTime, bytesSent, bytesReceived)
+ priceCHF = model.calculatepriceCHF(processingTime, bytesSent, bytesReceived)
# Create AiCallResponse with real calculation
# Use model.name for the response (API identifier), not displayName
aiResponse = AiCallResponse(
content="", # No content for extraction stats needed
modelName=model.name,
- priceUsd=priceUsd,
+ priceCHF=priceCHF,
processingTime=processingTime,
bytesSent=bytesSent,
bytesReceived=bytesReceived,
@@ -478,7 +478,7 @@ class ExtractionService:
"resultSize": len(response.content),
"typeGroup": part.typeGroup,
"modelName": response.modelName,
- "priceUsd": response.priceUsd
+ "priceCHF": response.priceCHF
}
)
@@ -606,7 +606,7 @@ class ExtractionService:
"originalIndex": i, # Phase 7: Explicit order index
"processingOrder": i, # Phase 7: Processing order
"modelName": result.modelName,
- "priceUsd": result.priceUsd,
+ "priceCHF": result.priceCHF,
"processingTime": result.processingTime,
"bytesSent": result.bytesSent,
"bytesReceived": result.bytesReceived
@@ -1311,7 +1311,7 @@ class ExtractionService:
return AiCallResponse(
content=modelResponse.content,
modelName=model.name,
- priceUsd=0.0,
+ priceCHF=0.0,
processingTime=processingTime,
bytesSent=0,
bytesReceived=0,
@@ -1416,7 +1416,7 @@ class ExtractionService:
return AiCallResponse(
content=mergedContent,
modelName=model.name,
- priceUsd=sum(r.priceUsd for r in chunkResults),
+ priceCHF=sum(r.priceCHF for r in chunkResults),
processingTime=sum(r.processingTime for r in chunkResults),
bytesSent=sum(r.bytesSent for r in chunkResults),
bytesReceived=sum(r.bytesReceived for r in chunkResults),
@@ -1465,7 +1465,7 @@ class ExtractionService:
return AiCallResponse(
content=mergedContent,
modelName=model.name,
- priceUsd=sum(r.priceUsd for r in chunkResults),
+ priceCHF=sum(r.priceCHF for r in chunkResults),
processingTime=sum(r.processingTime for r in chunkResults),
bytesSent=sum(r.bytesSent for r in chunkResults),
bytesReceived=sum(r.bytesReceived for r in chunkResults),
@@ -1492,7 +1492,7 @@ class ExtractionService:
return AiCallResponse(
content=errorMsg,
modelName="error",
- priceUsd=0.0,
+ priceCHF=0.0,
processingTime=0.0,
bytesSent=inputBytes,
bytesReceived=outputBytes,
@@ -1622,7 +1622,7 @@ class ExtractionService:
return AiCallResponse(
content=mergedContent,
modelName="multiple",
- priceUsd=sum(r.priceUsd for r in allResults),
+ priceCHF=sum(r.priceCHF for r in allResults),
processingTime=sum(r.processingTime for r in allResults),
bytesSent=sum(r.bytesSent for r in allResults),
bytesReceived=sum(r.bytesReceived for r in allResults),
diff --git a/modules/shared/gdprDeletion.py b/modules/shared/gdprDeletion.py
index da8a60cf..034b627a 100644
--- a/modules/shared/gdprDeletion.py
+++ b/modules/shared/gdprDeletion.py
@@ -576,22 +576,16 @@ def _deleteUserDataFromFeatureDatabases(userId: str, currentUser) -> Dict[str, A
rootInterface = getRootInterface()
- # Get all feature accesses for this user
- featureAccesses = rootInterface.db.getRecordset(
- FeatureAccess,
- recordFilter={"userId": str(userId)}
- )
+ # Get all feature accesses for this user using interface method
+ featureAccesses = rootInterface.getFeatureAccessesForUser(str(userId))
# Collect unique feature codes
featureCodes: Set[str] = set()
for fa in featureAccesses:
- instanceId = fa.get("featureInstanceId")
- instanceRecords = rootInterface.db.getRecordset(
- FeatureInstance,
- recordFilter={"id": instanceId}
- )
- if instanceRecords:
- featureCode = instanceRecords[0].get("featureCode")
+ instanceId = fa.featureInstanceId
+ instance = rootInterface.getFeatureInstance(instanceId)
+ if instance:
+ featureCode = instance.featureCode
if featureCode:
featureCodes.add(featureCode)
diff --git a/modules/system/mainSystem.py b/modules/system/mainSystem.py
index 113fa903..9b300d78 100644
--- a/modules/system/mainSystem.py
+++ b/modules/system/mainSystem.py
@@ -25,11 +25,11 @@ FEATURE_ICON = "mdi-cog"
# Block Order (gemäss Navigation-API-Konzept):
# - System: 10
# - : 15 (wird in routeSystem.py eingefügt)
-# - Workflows: 20
# - Basisdaten: 30
-# - Migrate: 40
# - Administration: 200
#
+# NOTE: Workflows and Migrate sections removed - now handled as features
+#
# Item Order: Default-Abstand 10 pro Item
# uiComponent: Abgeleitet von objectKey (ui.system.home -> page.system.home)
# icon: Wird intern gehalten aber NICHT in der API Response zurückgegeben
@@ -60,49 +60,6 @@ NAVIGATION_SECTIONS = [
},
],
},
- {
- "id": "workflows",
- "title": {"en": "WORKFLOWS", "de": "WORKFLOWS", "fr": "WORKFLOWS"},
- "order": 20,
- "items": [
- {
- "id": "playground",
- "objectKey": "ui.system.playground",
- "label": {"en": "Chat Playground", "de": "Chat Playground", "fr": "Chat Playground"},
- "icon": "FaPlay",
- "path": "/workflows/playground",
- "order": 10,
- "public": True,
- },
- {
- "id": "chats",
- "objectKey": "ui.system.chats",
- "label": {"en": "Chats", "de": "Chats", "fr": "Chats"},
- "icon": "FaListAlt",
- "path": "/workflows/list",
- "order": 20,
- "public": True,
- },
- {
- "id": "automations",
- "objectKey": "ui.system.automations",
- "label": {"en": "Automations", "de": "Automatisierungen", "fr": "Automatisations"},
- "icon": "FaCogs",
- "path": "/workflows/automations",
- "order": 30,
- "public": True,
- },
- {
- "id": "automation-templates",
- "objectKey": "ui.system.automation-templates",
- "label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
- "icon": "FaFileAlt",
- "path": "/workflows/automation-templates",
- "order": 35,
- "public": True,
- },
- ],
- },
{
"id": "basedata",
"title": {"en": "BASE DATA", "de": "BASISDATEN", "fr": "DONNÉES DE BASE"},
@@ -134,54 +91,55 @@ NAVIGATION_SECTIONS = [
},
],
},
- {
- "id": "migrate",
- "title": {"en": "MIGRATE TO FEATURES", "de": "MIGRATE TO FEATURES", "fr": "MIGRER VERS FEATURES"},
- "order": 40,
- "deprecated": True,
- "items": [
- {
- "id": "chatbot",
- "objectKey": "ui.system.chatbot",
- "label": {"en": "Chatbot", "de": "Chatbot", "fr": "Chatbot"},
- "icon": "FaComments",
- "path": "/chatbot",
- "order": 10,
- "deprecated": True,
- },
- {
- "id": "pek",
- "objectKey": "ui.system.pek",
- "label": {"en": "PEK", "de": "PEK", "fr": "PEK"},
- "icon": "FaChartBar",
- "path": "/pek",
- "order": 20,
- "deprecated": True,
- },
- {
- "id": "speech",
- "objectKey": "ui.system.speech",
- "label": {"en": "Speech", "de": "Sprache", "fr": "Parole"},
- "icon": "FaMicrophone",
- "path": "/speech",
- "order": 30,
- "deprecated": True,
- },
- ],
- },
{
"id": "admin",
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
"order": 200,
"adminOnly": True,
"items": [
+ {
+ "id": "admin-users",
+ "objectKey": "ui.admin.users",
+ "label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
+ "icon": "FaUsers",
+ "path": "/admin/users",
+ "order": 10,
+ "adminOnly": True,
+ },
+ {
+ "id": "admin-invitations",
+ "objectKey": "ui.admin.invitations",
+ "label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
+ "icon": "FaEnvelopeOpenText",
+ "path": "/admin/invitations",
+ "order": 12,
+ "adminOnly": True,
+ },
+ {
+ "id": "admin-user-access-overview",
+ "objectKey": "ui.admin.userAccessOverview",
+ "label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
+ "icon": "FaClipboardList",
+ "path": "/admin/user-access-overview",
+ "order": 14,
+ "adminOnly": True,
+ },
{
"id": "admin-mandates",
"objectKey": "ui.admin.mandates",
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
"icon": "FaBuilding",
"path": "/admin/mandates",
- "order": 3,
+ "order": 20,
+ "adminOnly": True,
+ },
+ {
+ "id": "admin-user-mandates",
+ "objectKey": "ui.admin.userMandates",
+ "label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
+ "icon": "FaUserFriends",
+ "path": "/admin/user-mandates",
+ "order": 25,
"adminOnly": True,
},
{
@@ -190,27 +148,36 @@ NAVIGATION_SECTIONS = [
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
"icon": "FaBuilding",
"path": "/admin/access",
- "order": 5,
- "adminOnly": True,
- },
- {
- "id": "admin-users",
- "objectKey": "ui.admin.users",
- "label": {"en": "Users & Invitations", "de": "Benutzer & Einladungen", "fr": "Utilisateurs et invitations"},
- "icon": "FaUsers",
- "path": "/admin/users",
- "order": 10,
+ "order": 30,
"adminOnly": True,
},
{
"id": "admin-roles",
"objectKey": "ui.admin.roles",
- "label": {"en": "Roles & Permissions", "de": "Rollen & Berechtigungen", "fr": "Rôles et permissions"},
- "icon": "FaKey",
+ "label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
+ "icon": "FaUserTag",
"path": "/admin/mandate-roles",
"order": 40,
"adminOnly": True,
},
+ {
+ "id": "admin-mandate-role-permissions",
+ "objectKey": "ui.admin.mandateRolePermissions",
+ "label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
+ "icon": "FaKey",
+ "path": "/admin/mandate-role-permissions",
+ "order": 45,
+ "adminOnly": True,
+ },
+ {
+ "id": "admin-feature-roles",
+ "objectKey": "ui.admin.featureRoles",
+ "label": {"en": "Feature Roles & Permissions", "de": "Features Rollen & Rechte", "fr": "Rôles et droits des features"},
+ "icon": "FaShieldAlt",
+ "path": "/admin/feature-roles",
+ "order": 50,
+ "adminOnly": True,
+ },
],
},
]
diff --git a/modules/workflows/methods/methodAi/actions/process.py b/modules/workflows/methods/methodAi/actions/process.py
index b85761b7..752fe7f6 100644
--- a/modules/workflows/methods/methodAi/actions/process.py
+++ b/modules/workflows/methods/methodAi/actions/process.py
@@ -153,7 +153,7 @@ async def process(self, parameters: Dict[str, Any]) -> ActionResult:
metadata=AiResponseMetadata(
additionalData={
"modelName": aiResponse_obj.modelName,
- "priceUsd": aiResponse_obj.priceUsd,
+ "priceCHF": aiResponse_obj.priceCHF,
"processingTime": aiResponse_obj.processingTime,
"bytesSent": aiResponse_obj.bytesSent,
"bytesReceived": aiResponse_obj.bytesReceived,
diff --git a/tests/functional/test02_ai_models.py b/tests/functional/test02_ai_models.py
index 12a374f8..f1b2f62f 100644
--- a/tests/functional/test02_ai_models.py
+++ b/tests/functional/test02_ai_models.py
@@ -628,7 +628,7 @@ Width: {crawlWidth}
"hasContent": True,
"error": None,
"modelUsed": modelName,
- "priceUsd": 0.0,
+ "priceCHF": 0.0,
"bytesSent": 0,
"bytesReceived": contentLength,
"isValidJson": True,