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,