cleaned up all route and main references - no direct access to db.getRecordset - only over interfaces
This commit is contained in:
parent
555c9429fb
commit
45eda1e4d4
46 changed files with 2462 additions and 1316 deletions
7
app.py
7
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.)
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
modules/features/chatplayground/__init__.py
Normal file
6
modules/features/chatplayground/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Chat Playground Feature Container.
|
||||
Provides workflow-based chat playground functionality.
|
||||
"""
|
||||
|
|
@ -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)
|
||||
273
modules/features/chatplayground/mainChatplayground.py
Normal file
273
modules/features/chatplayground/mainChatplayground.py
Normal file
|
|
@ -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
|
||||
233
modules/features/chatplayground/routeFeatureChatplayground.py
Normal file
233
modules/features/chatplayground/routeFeatureChatplayground.py
Normal file
|
|
@ -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)}"
|
||||
)
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}"
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ FEATURE_ICON = "mdi-cog"
|
|||
# Block Order (gemäss Navigation-API-Konzept):
|
||||
# - System: 10
|
||||
# - <dynamic/features>: 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -628,7 +628,7 @@ Width: {crawlWidth}
|
|||
"hasContent": True,
|
||||
"error": None,
|
||||
"modelUsed": modelName,
|
||||
"priceUsd": 0.0,
|
||||
"priceCHF": 0.0,
|
||||
"bytesSent": 0,
|
||||
"bytesReceived": contentLength,
|
||||
"isValidJson": True,
|
||||
|
|
|
|||
Loading…
Reference in a new issue