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
|
from modules.routes.routeMessaging import router as messagingRouter
|
||||||
app.include_router(messagingRouter)
|
app.include_router(messagingRouter)
|
||||||
|
|
||||||
# Phase 8: New Feature Routes
|
|
||||||
from modules.routes.routeAdminFeatures import router as featuresAdminRouter
|
from modules.routes.routeAdminFeatures import router as featuresAdminRouter
|
||||||
app.include_router(featuresAdminRouter)
|
app.include_router(featuresAdminRouter)
|
||||||
|
|
||||||
|
|
@ -504,12 +503,6 @@ app.include_router(userAccessOverviewRouter)
|
||||||
from modules.routes.routeGdpr import router as gdprRouter
|
from modules.routes.routeGdpr import router as gdprRouter
|
||||||
app.include_router(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.)
|
# SYSTEM ROUTES (Navigation, etc.)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class AiAnthropic(BaseConnectorAi):
|
||||||
(OperationTypeEnum.DATA_EXTRACT, 8)
|
(OperationTypeEnum.DATA_EXTRACT, 8)
|
||||||
),
|
),
|
||||||
version="claude-sonnet-4-5-20250929",
|
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(
|
AiModel(
|
||||||
name="claude-sonnet-4-5-20250929",
|
name="claude-sonnet-4-5-20250929",
|
||||||
|
|
@ -93,7 +93,7 @@ class AiAnthropic(BaseConnectorAi):
|
||||||
(OperationTypeEnum.IMAGE_ANALYSE, 10)
|
(OperationTypeEnum.IMAGE_ANALYSE, 10)
|
||||||
),
|
),
|
||||||
version="claude-sonnet-4-5-20250929",
|
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,
|
processingMode=ProcessingModeEnum.BASIC,
|
||||||
operationTypes=createOperationTypeRatings(),
|
operationTypes=createOperationTypeRatings(),
|
||||||
version="internal-extractor-v1",
|
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(
|
AiModel(
|
||||||
name="internal-generator",
|
name="internal-generator",
|
||||||
|
|
@ -60,7 +60,7 @@ class AiInternal(BaseConnectorAi):
|
||||||
processingMode=ProcessingModeEnum.BASIC,
|
processingMode=ProcessingModeEnum.BASIC,
|
||||||
operationTypes=createOperationTypeRatings(),
|
operationTypes=createOperationTypeRatings(),
|
||||||
version="internal-generator-v1",
|
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(
|
AiModel(
|
||||||
name="internal-renderer",
|
name="internal-renderer",
|
||||||
|
|
@ -80,7 +80,7 @@ class AiInternal(BaseConnectorAi):
|
||||||
processingMode=ProcessingModeEnum.DETAILED,
|
processingMode=ProcessingModeEnum.DETAILED,
|
||||||
operationTypes=createOperationTypeRatings(),
|
operationTypes=createOperationTypeRatings(),
|
||||||
version="internal-renderer-v1",
|
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)
|
(OperationTypeEnum.DATA_EXTRACT, 7)
|
||||||
),
|
),
|
||||||
version="gpt-4o",
|
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(
|
AiModel(
|
||||||
name="gpt-3.5-turbo",
|
name="gpt-3.5-turbo",
|
||||||
|
|
@ -97,7 +97,7 @@ class AiOpenai(BaseConnectorAi):
|
||||||
# Note: GPT-3.5-turbo does NOT support vision/image operations
|
# Note: GPT-3.5-turbo does NOT support vision/image operations
|
||||||
),
|
),
|
||||||
version="gpt-3.5-turbo",
|
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(
|
AiModel(
|
||||||
name="gpt-4o",
|
name="gpt-4o",
|
||||||
|
|
@ -118,7 +118,7 @@ class AiOpenai(BaseConnectorAi):
|
||||||
(OperationTypeEnum.IMAGE_ANALYSE, 9)
|
(OperationTypeEnum.IMAGE_ANALYSE, 9)
|
||||||
),
|
),
|
||||||
version="gpt-4o",
|
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(
|
AiModel(
|
||||||
name="dall-e-3",
|
name="dall-e-3",
|
||||||
|
|
@ -140,7 +140,7 @@ class AiOpenai(BaseConnectorAi):
|
||||||
(OperationTypeEnum.IMAGE_GENERATE, 10)
|
(OperationTypeEnum.IMAGE_GENERATE, 10)
|
||||||
),
|
),
|
||||||
version="dall-e-3",
|
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)
|
(OperationTypeEnum.WEB_CRAWL, 7)
|
||||||
),
|
),
|
||||||
version="sonar",
|
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(
|
AiModel(
|
||||||
name="sonar-pro",
|
name="sonar-pro",
|
||||||
|
|
@ -97,7 +97,7 @@ class AiPerplexity(BaseConnectorAi):
|
||||||
(OperationTypeEnum.WEB_CRAWL, 8)
|
(OperationTypeEnum.WEB_CRAWL, 8)
|
||||||
),
|
),
|
||||||
version="sonar-pro",
|
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)
|
(OperationTypeEnum.WEB_CRAWL, 10)
|
||||||
),
|
),
|
||||||
version="tavily-search",
|
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)
|
# Function reference (not serialized)
|
||||||
functionCall: Optional[Callable] = Field(default=None, exclude=True, description="Function to call for this model")
|
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
|
# Selection criteria - capabilities with ratings
|
||||||
priority: PriorityEnum = Field(default=PriorityEnum.BALANCED, description="Default priority for this model. See PriorityEnum for available values.")
|
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")
|
content: str = Field(description="AI response content")
|
||||||
modelName: str = Field(description="Selected model name")
|
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")
|
processingTime: float = Field(default=0.0, description="Duration in seconds")
|
||||||
bytesSent: int = Field(default=0, description="Input data size in bytes")
|
bytesSent: int = Field(default=0, description="Input data size in bytes")
|
||||||
bytesReceived: int = Field(default=0, description="Output 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")
|
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')")
|
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')")
|
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(
|
registerModelLabels(
|
||||||
|
|
@ -41,7 +41,7 @@ registerModelLabels(
|
||||||
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"},
|
"errorCount": {"en": "Error Count", "fr": "Nombre d'erreurs"},
|
||||||
"process": {"en": "Process", "fr": "Processus"},
|
"process": {"en": "Process", "fr": "Processus"},
|
||||||
"engine": {"en": "Engine", "fr": "Moteur"},
|
"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
|
# Template roles for this feature
|
||||||
|
# IMPORTANT: "viewer" role is required for automatic user assignment!
|
||||||
TEMPLATE_ROLES = [
|
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",
|
"roleLabel": "automation-admin",
|
||||||
"description": {
|
"description": {
|
||||||
|
|
@ -161,9 +178,132 @@ def registerFeature(catalogService) -> bool:
|
||||||
meta=resObj.get("meta")
|
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")
|
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
|
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
|
||||||
return False
|
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):
|
async def _getSharepointConnection(self, sharepointPath: str = None):
|
||||||
try:
|
try:
|
||||||
connections = self.services.interfaceDbApp.db.getRecordset(
|
# Use interface method to get user connections
|
||||||
UserConnection,
|
connections = self.services.interfaceDbApp.getUserConnections(self.services.interfaceDbApp.userId)
|
||||||
recordFilter={"userId": self.services.interfaceDbApp.userId}
|
msftConnections = [c for c in connections if c.authority == 'msft']
|
||||||
)
|
|
||||||
msftConnections = [c for c in connections if c.get('authority') == 'msft']
|
|
||||||
if not msftConnections:
|
if not msftConnections:
|
||||||
logger.warning('No Microsoft connections found for user')
|
logger.warning('No Microsoft connections found for user')
|
||||||
return None
|
return None
|
||||||
if len(msftConnections) == 1:
|
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]
|
return msftConnections[0]
|
||||||
if sharepointPath:
|
if sharepointPath:
|
||||||
return await self._matchConnectionToPath(msftConnections, 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]
|
return msftConnections[0]
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error('Error getting SharePoint connection')
|
logger.error('Error getting SharePoint connection')
|
||||||
|
|
|
||||||
|
|
@ -165,13 +165,11 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
db = rootInterface.db
|
|
||||||
|
|
||||||
existingRoles = db.getRecordset(
|
# Get existing template roles (Pydantic models)
|
||||||
Role,
|
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
|
templateRoles = [r for r in existingRoles if r.mandateId is None]
|
||||||
)
|
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
|
||||||
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
|
|
||||||
|
|
||||||
createdCount = 0
|
createdCount = 0
|
||||||
for roleTemplate in TEMPLATE_ROLES:
|
for roleTemplate in TEMPLATE_ROLES:
|
||||||
|
|
@ -179,7 +177,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
|
|
||||||
if roleLabel in existingRoleLabels:
|
if roleLabel in existingRoleLabels:
|
||||||
roleId = existingRoleLabels[roleLabel]
|
roleId = existingRoleLabels[roleLabel]
|
||||||
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
|
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
|
||||||
else:
|
else:
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
roleLabel=roleLabel,
|
roleLabel=roleLabel,
|
||||||
|
|
@ -189,65 +187,65 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
isSystemRole=False
|
isSystemRole=False
|
||||||
)
|
)
|
||||||
createdRole = db.recordCreate(Role, newRole.model_dump())
|
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
|
||||||
roleId = createdRole.get("id")
|
roleId = createdRole.get("id")
|
||||||
existingRoleLabels[roleLabel] = roleId
|
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}")
|
logging.getLogger(__name__).info(f"Created template role '{roleLabel}' with ID {roleId}")
|
||||||
createdCount += 1
|
createdCount += 1
|
||||||
|
|
||||||
if createdCount > 0:
|
if createdCount > 0:
|
||||||
logging.getLogger(__name__).info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
|
logging.getLogger(__name__).info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
|
||||||
|
|
||||||
_repairInstanceRolesAccessRules(db, existingRoleLabels)
|
_repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
|
||||||
return createdCount
|
return createdCount
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.getLogger(__name__).error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
|
logging.getLogger(__name__).error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
|
||||||
return 0
|
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."""
|
"""Repair instance-specific roles by copying AccessRules from their template roles."""
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule
|
from modules.datamodels.datamodelRbac import Role, AccessRule
|
||||||
|
|
||||||
repairedCount = 0
|
repairedCount = 0
|
||||||
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
|
allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
|
instanceRoles = [r for r in allRoles if r.mandateId is not None]
|
||||||
|
|
||||||
for instanceRole in instanceRoles:
|
for instanceRole in instanceRoles:
|
||||||
roleLabel = instanceRole.get("roleLabel")
|
roleLabel = instanceRole.roleLabel
|
||||||
instanceRoleId = instanceRole.get("id")
|
instanceRoleId = str(instanceRole.id)
|
||||||
templateRoleId = templateRoleLabels.get(roleLabel)
|
templateRoleId = templateRoleLabels.get(roleLabel)
|
||||||
if not templateRoleId:
|
if not templateRoleId:
|
||||||
continue
|
continue
|
||||||
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
|
existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
|
||||||
if existingRules:
|
if existingRules:
|
||||||
continue
|
continue
|
||||||
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
|
templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
|
||||||
if not templateRules:
|
if not templateRules:
|
||||||
continue
|
continue
|
||||||
for rule in templateRules:
|
for rule in templateRules:
|
||||||
newRule = AccessRule(
|
newRule = AccessRule(
|
||||||
roleId=instanceRoleId,
|
roleId=instanceRoleId,
|
||||||
context=rule.get("context"),
|
context=rule.context,
|
||||||
item=rule.get("item"),
|
item=rule.item,
|
||||||
view=rule.get("view", False),
|
view=rule.view if rule.view else False,
|
||||||
read=rule.get("read"),
|
read=rule.read,
|
||||||
create=rule.get("create"),
|
create=rule.create,
|
||||||
update=rule.get("update"),
|
update=rule.update,
|
||||||
delete=rule.get("delete"),
|
delete=rule.delete,
|
||||||
)
|
)
|
||||||
db.recordCreate(AccessRule, newRule.model_dump())
|
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
repairedCount += 1
|
repairedCount += 1
|
||||||
return repairedCount
|
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."""
|
"""Ensure AccessRules exist for a role based on templates."""
|
||||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||||
|
|
||||||
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
existingRules = rootInterface.getAccessRulesByRole(roleId)
|
||||||
existingSignatures = {(r.get("context"), r.get("item")) for r in existingRules}
|
existingSignatures = {(str(r.context) if r.context else None, r.item) for r in existingRules}
|
||||||
createdCount = 0
|
createdCount = 0
|
||||||
|
|
||||||
for template in ruleTemplates or []:
|
for template in ruleTemplates or []:
|
||||||
|
|
@ -273,7 +271,7 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: list) -> int:
|
||||||
update=template.get("update"),
|
update=template.get("update"),
|
||||||
delete=template.get("delete"),
|
delete=template.get("delete"),
|
||||||
)
|
)
|
||||||
db.recordCreate(AccessRule, newRule.model_dump())
|
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
createdCount += 1
|
createdCount += 1
|
||||||
existingSignatures.add((context, item))
|
existingSignatures.add((context, item))
|
||||||
return createdCount
|
return createdCount
|
||||||
|
|
|
||||||
|
|
@ -267,14 +267,11 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
db = rootInterface.db
|
|
||||||
|
|
||||||
# Get existing template roles for this feature
|
# Get existing template roles for this feature (Pydantic models)
|
||||||
existingRoles = db.getRecordset(
|
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
Role,
|
templateRoles = [r for r in existingRoles if r.mandateId is None]
|
||||||
recordFilter={"featureCode": FEATURE_CODE, "mandateId": None}
|
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
|
||||||
)
|
|
||||||
existingRoleLabels = {r.get("roleLabel"): r.get("id") for r in existingRoles}
|
|
||||||
|
|
||||||
createdCount = 0
|
createdCount = 0
|
||||||
for roleTemplate in TEMPLATE_ROLES:
|
for roleTemplate in TEMPLATE_ROLES:
|
||||||
|
|
@ -285,7 +282,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
|
logger.debug(f"Template role '{roleLabel}' already exists with ID {roleId}")
|
||||||
|
|
||||||
# Ensure AccessRules exist for this role
|
# Ensure AccessRules exist for this role
|
||||||
_ensureAccessRulesForRole(db, roleId, roleTemplate.get("accessRules", []))
|
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
|
||||||
else:
|
else:
|
||||||
# Create new template role
|
# Create new template role
|
||||||
newRole = Role(
|
newRole = Role(
|
||||||
|
|
@ -296,11 +293,11 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
featureInstanceId=None,
|
featureInstanceId=None,
|
||||||
isSystemRole=False
|
isSystemRole=False
|
||||||
)
|
)
|
||||||
createdRole = db.recordCreate(Role, newRole.model_dump())
|
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
|
||||||
roleId = createdRole.get("id")
|
roleId = createdRole.get("id")
|
||||||
|
|
||||||
# Create AccessRules for this role
|
# 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}")
|
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
|
||||||
createdCount += 1
|
createdCount += 1
|
||||||
|
|
@ -309,7 +306,7 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
|
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
|
||||||
|
|
||||||
# Repair instance-specific roles that are missing AccessRules
|
# Repair instance-specific roles that are missing AccessRules
|
||||||
_repairInstanceRolesAccessRules(db, existingRoleLabels)
|
_repairInstanceRolesAccessRules(rootInterface, existingRoleLabels)
|
||||||
|
|
||||||
return createdCount
|
return createdCount
|
||||||
|
|
||||||
|
|
@ -318,13 +315,13 @@ def _syncTemplateRolesToDb() -> int:
|
||||||
return 0
|
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.
|
Repair instance-specific roles by copying AccessRules from their template roles.
|
||||||
This ensures instance roles created before AccessRules were defined get updated.
|
This ensures instance roles created before AccessRules were defined get updated.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: Database connector
|
rootInterface: Root interface instance
|
||||||
templateRoleLabels: Dict mapping roleLabel to template role ID
|
templateRoleLabels: Dict mapping roleLabel to template role ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -334,41 +331,41 @@ def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> i
|
||||||
|
|
||||||
repairedCount = 0
|
repairedCount = 0
|
||||||
|
|
||||||
# Get all instance-specific roles for this feature (mandateId is NOT None)
|
# Get all instance-specific roles for this feature (Pydantic models)
|
||||||
allRoles = db.getRecordset(Role, recordFilter={"featureCode": FEATURE_CODE})
|
allRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
|
||||||
instanceRoles = [r for r in allRoles if r.get("mandateId") is not None]
|
instanceRoles = [r for r in allRoles if r.mandateId is not None]
|
||||||
|
|
||||||
for instanceRole in instanceRoles:
|
for instanceRole in instanceRoles:
|
||||||
roleLabel = instanceRole.get("roleLabel")
|
roleLabel = instanceRole.roleLabel
|
||||||
instanceRoleId = instanceRole.get("id")
|
instanceRoleId = str(instanceRole.id)
|
||||||
|
|
||||||
# Find matching template role
|
# Find matching template role
|
||||||
templateRoleId = templateRoleLabels.get(roleLabel)
|
templateRoleId = templateRoleLabels.get(roleLabel)
|
||||||
if not templateRoleId:
|
if not templateRoleId:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if instance role has AccessRules
|
# Check if instance role has AccessRules (Pydantic models)
|
||||||
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": instanceRoleId})
|
existingRules = rootInterface.getAccessRulesByRole(instanceRoleId)
|
||||||
if existingRules:
|
if existingRules:
|
||||||
continue # Already has rules, skip
|
continue # Already has rules, skip
|
||||||
|
|
||||||
# Copy AccessRules from template role
|
# Copy AccessRules from template role (Pydantic models)
|
||||||
templateRules = db.getRecordset(AccessRule, recordFilter={"roleId": templateRoleId})
|
templateRules = rootInterface.getAccessRulesByRole(templateRoleId)
|
||||||
if not templateRules:
|
if not templateRules:
|
||||||
continue # Template has no rules
|
continue # Template has no rules
|
||||||
|
|
||||||
for rule in templateRules:
|
for rule in templateRules:
|
||||||
newRule = AccessRule(
|
newRule = AccessRule(
|
||||||
roleId=instanceRoleId,
|
roleId=instanceRoleId,
|
||||||
context=rule.get("context"),
|
context=rule.context,
|
||||||
item=rule.get("item"),
|
item=rule.item,
|
||||||
view=rule.get("view", False),
|
view=rule.view if rule.view else False,
|
||||||
read=rule.get("read"),
|
read=rule.read,
|
||||||
create=rule.get("create"),
|
create=rule.create,
|
||||||
update=rule.get("update"),
|
update=rule.update,
|
||||||
delete=rule.get("delete"),
|
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")
|
logger.info(f"Repaired instance role '{roleLabel}' (ID: {instanceRoleId}): copied {len(templateRules)} AccessRules from template")
|
||||||
repairedCount += 1
|
repairedCount += 1
|
||||||
|
|
@ -379,12 +376,12 @@ def _repairInstanceRolesAccessRules(db, templateRoleLabels: Dict[str, str]) -> i
|
||||||
return repairedCount
|
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.
|
Ensure AccessRules exist for a role based on templates.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: Database connector
|
rootInterface: Root interface instance
|
||||||
roleId: Role ID
|
roleId: Role ID
|
||||||
ruleTemplates: List of rule templates
|
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
|
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
|
||||||
|
|
||||||
# Get existing rules for this role
|
# Get existing rules for this role (Pydantic models)
|
||||||
existingRules = db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
existingRules = rootInterface.getAccessRulesByRole(roleId)
|
||||||
|
|
||||||
# Create a set of existing rule signatures to avoid duplicates
|
# Create a set of existing rule signatures to avoid duplicates
|
||||||
existingSignatures = set()
|
existingSignatures = set()
|
||||||
for rule in existingRules:
|
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)
|
existingSignatures.add(sig)
|
||||||
|
|
||||||
createdCount = 0
|
createdCount = 0
|
||||||
|
|
@ -431,7 +428,7 @@ def _ensureAccessRulesForRole(db, roleId: str, ruleTemplates: List[Dict[str, Any
|
||||||
update=template.get("update"),
|
update=template.get("update"),
|
||||||
delete=template.get("delete"),
|
delete=template.get("delete"),
|
||||||
)
|
)
|
||||||
db.recordCreate(AccessRule, newRule.model_dump())
|
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
|
||||||
createdCount += 1
|
createdCount += 1
|
||||||
|
|
||||||
if createdCount > 0:
|
if createdCount > 0:
|
||||||
|
|
|
||||||
|
|
@ -1363,17 +1363,11 @@ async def get_instance_roles(
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get instance-specific roles (mandateId set, featureInstanceId matches)
|
# Get instance-specific roles (Pydantic models)
|
||||||
roles = rootInterface.db.getRecordset(
|
roles = rootInterface.getRolesByFeatureCode("trustee", featureInstanceId=instanceId)
|
||||||
Role,
|
|
||||||
recordFilter={
|
|
||||||
"featureCode": "trustee",
|
|
||||||
"featureInstanceId": instanceId
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return PaginatedResponse(
|
return PaginatedResponse(
|
||||||
items=roles,
|
items=[r.model_dump() for r in roles],
|
||||||
pagination=None
|
pagination=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1390,18 +1384,16 @@ async def get_instance_role(
|
||||||
mandateId = await _validateInstanceAdmin(instanceId, context)
|
mandateId = await _validateInstanceAdmin(instanceId, context)
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
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")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
|
||||||
|
|
||||||
role = roles[0]
|
|
||||||
|
|
||||||
# Verify role belongs to this instance
|
# 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")
|
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)
|
@router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse)
|
||||||
|
|
@ -1420,19 +1412,16 @@ async def get_instance_role_rules(
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Verify role belongs to this instance
|
# Verify role belongs to this instance (Pydantic model)
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if not roles or roles[0].get("featureInstanceId") != instanceId:
|
if not role or str(role.featureInstanceId) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
||||||
|
|
||||||
# Get AccessRules for this role
|
# Get AccessRules for this role (Pydantic models)
|
||||||
rules = rootInterface.db.getRecordset(
|
rules = rootInterface.getAccessRulesByRole(roleId)
|
||||||
AccessRule,
|
|
||||||
recordFilter={"roleId": roleId}
|
|
||||||
)
|
|
||||||
|
|
||||||
return PaginatedResponse(
|
return PaginatedResponse(
|
||||||
items=rules,
|
items=[r.model_dump() for r in rules],
|
||||||
pagination=None
|
pagination=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1454,9 +1443,9 @@ async def create_instance_role_rule(
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Verify role belongs to this instance
|
# Verify role belongs to this instance (Pydantic model)
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if not roles or roles[0].get("featureInstanceId") != instanceId:
|
if not role or str(role.featureInstanceId) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
||||||
|
|
||||||
# Create the rule
|
# Create the rule
|
||||||
|
|
@ -1505,14 +1494,14 @@ async def update_instance_role_rule(
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Verify role belongs to this instance
|
# Verify role belongs to this instance (Pydantic model)
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if not roles or roles[0].get("featureInstanceId") != instanceId:
|
if not role or str(role.featureInstanceId) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
||||||
|
|
||||||
# Verify rule belongs to role
|
# Verify rule belongs to role (Pydantic model)
|
||||||
existingRules = rootInterface.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
|
existingRule = rootInterface.getAccessRule(ruleId)
|
||||||
if not existingRules or existingRules[0].get("roleId") != roleId:
|
if not existingRule or str(existingRule.roleId) != roleId:
|
||||||
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
|
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
|
||||||
|
|
||||||
# Update only allowed fields
|
# Update only allowed fields
|
||||||
|
|
@ -1529,7 +1518,7 @@ async def update_instance_role_rule(
|
||||||
updateData["delete"] = ruleData["delete"]
|
updateData["delete"] = ruleData["delete"]
|
||||||
|
|
||||||
if not updateData:
|
if not updateData:
|
||||||
return existingRules[0]
|
return existingRule.model_dump()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
updated = rootInterface.db.recordModify(AccessRule, ruleId, updateData)
|
updated = rootInterface.db.recordModify(AccessRule, ruleId, updateData)
|
||||||
|
|
@ -1556,14 +1545,14 @@ async def delete_instance_role_rule(
|
||||||
|
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Verify role belongs to this instance
|
# Verify role belongs to this instance (Pydantic model)
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if not roles or roles[0].get("featureInstanceId") != instanceId:
|
if not role or str(role.featureInstanceId) != instanceId:
|
||||||
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
raise HTTPException(status_code=404, detail=f"Role {roleId} not found in this instance")
|
||||||
|
|
||||||
# Verify rule belongs to role
|
# Verify rule belongs to role (Pydantic model)
|
||||||
existingRules = rootInterface.db.getRecordset(AccessRule, recordFilter={"id": ruleId})
|
existingRule = rootInterface.getAccessRule(ruleId)
|
||||||
if not existingRules or existingRules[0].get("roleId") != roleId:
|
if not existingRule or str(existingRule.roleId) != roleId:
|
||||||
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
|
raise HTTPException(status_code=404, detail=f"Rule {ruleId} not found for this role")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ class AiObjects:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=errorMsg,
|
content=errorMsg,
|
||||||
modelName="error",
|
modelName="error",
|
||||||
priceUsd=0.0,
|
priceCHF=0.0,
|
||||||
processingTime=0.0,
|
processingTime=0.0,
|
||||||
bytesSent=0,
|
bytesSent=0,
|
||||||
bytesReceived=0,
|
bytesReceived=0,
|
||||||
|
|
@ -135,7 +135,7 @@ class AiObjects:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=errorMsg,
|
content=errorMsg,
|
||||||
modelName="error",
|
modelName="error",
|
||||||
priceUsd=0.0,
|
priceCHF=0.0,
|
||||||
processingTime=0.0,
|
processingTime=0.0,
|
||||||
bytesSent=0,
|
bytesSent=0,
|
||||||
bytesReceived=0,
|
bytesReceived=0,
|
||||||
|
|
@ -147,7 +147,7 @@ class AiObjects:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=errorMsg,
|
content=errorMsg,
|
||||||
modelName="error",
|
modelName="error",
|
||||||
priceUsd=0.0,
|
priceCHF=0.0,
|
||||||
processingTime=0.0,
|
processingTime=0.0,
|
||||||
bytesSent=inputBytes,
|
bytesSent=inputBytes,
|
||||||
bytesReceived=outputBytes,
|
bytesReceived=outputBytes,
|
||||||
|
|
@ -213,12 +213,12 @@ class AiObjects:
|
||||||
outputBytes = len(content.encode("utf-8"))
|
outputBytes = len(content.encode("utf-8"))
|
||||||
|
|
||||||
# Calculate price using model's own price calculation method
|
# Calculate price using model's own price calculation method
|
||||||
priceUsd = model.calculatePriceUsd(processingTime, inputBytes, outputBytes)
|
priceCHF = model.calculatepriceCHF(processingTime, inputBytes, outputBytes)
|
||||||
|
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=content,
|
content=content,
|
||||||
modelName=model.name,
|
modelName=model.name,
|
||||||
priceUsd=priceUsd,
|
priceCHF=priceCHF,
|
||||||
processingTime=processingTime,
|
processingTime=processingTime,
|
||||||
bytesSent=inputBytes,
|
bytesSent=inputBytes,
|
||||||
bytesReceived=outputBytes,
|
bytesReceived=outputBytes,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,10 @@ def initBootstrap(db: DatabaseConnector) -> None:
|
||||||
|
|
||||||
# Seed automation templates (after admin user exists)
|
# Seed automation templates (after admin user exists)
|
||||||
initAutomationTemplates(db, adminUserId)
|
initAutomationTemplates(db, adminUserId)
|
||||||
|
|
||||||
|
# Initialize feature instances for root mandate
|
||||||
|
if mandateId:
|
||||||
|
initRootMandateFeatures(db, mandateId)
|
||||||
|
|
||||||
|
|
||||||
def initAutomationTemplates(dbApp: DatabaseConnector, adminUserId: Optional[str] = None) -> None:
|
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")
|
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]:
|
def initRootMandate(db: DatabaseConnector) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Creates the Root mandate if it doesn't exist.
|
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.datamodelFeatures import Feature, FeatureInstance
|
||||||
from modules.datamodels.datamodelInvitation import Invitation
|
from modules.datamodels.datamodelInvitation import Invitation
|
||||||
|
from modules.datamodels.datamodelNotification import UserNotification
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -733,6 +734,9 @@ class AppObjects:
|
||||||
|
|
||||||
# Clear cache to ensure fresh data (already done above)
|
# 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])
|
return User(**createdUser[0])
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
@ -796,6 +800,99 @@ class AppObjects:
|
||||||
logger.error(f"Error updating user: {str(e)}")
|
logger.error(f"Error updating user: {str(e)}")
|
||||||
raise ValueError(f"Failed to update 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:
|
def disableUser(self, userId: str) -> User:
|
||||||
"""Disables a user if current user has permission."""
|
"""Disables a user if current user has permission."""
|
||||||
return self.updateUser(userId, {"enabled": False})
|
return self.updateUser(userId, {"enabled": False})
|
||||||
|
|
@ -1209,6 +1306,31 @@ class AppObjects:
|
||||||
logger.error(f"Error getting user connections: {str(e)}")
|
logger.error(f"Error getting user connections: {str(e)}")
|
||||||
return []
|
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(
|
def addUserConnection(
|
||||||
self,
|
self,
|
||||||
userId: str,
|
userId: str,
|
||||||
|
|
@ -1547,6 +1669,106 @@ class AppObjects:
|
||||||
logger.error(f"Error deleting UserMandate: {e}")
|
logger.error(f"Error deleting UserMandate: {e}")
|
||||||
raise ValueError(f"Failed to delete 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]:
|
def getRoleIdsForUserMandate(self, userMandateId: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get all role IDs assigned to a UserMandate.
|
Get all role IDs assigned to a UserMandate.
|
||||||
|
|
@ -1688,6 +1910,30 @@ class AppObjects:
|
||||||
logger.error(f"Error getting FeatureAccesses: {e}")
|
logger.error(f"Error getting FeatureAccesses: {e}")
|
||||||
return []
|
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:
|
def createFeatureAccess(self, userId: str, featureInstanceId: str, roleIds: List[str] = None) -> FeatureAccess:
|
||||||
"""
|
"""
|
||||||
Create a FeatureAccess record (grant user access to feature instance).
|
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}")
|
logger.error(f"Error getting role IDs for FeatureAccess: {e}")
|
||||||
return []
|
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
|
# Token methods
|
||||||
|
|
||||||
def saveAccessToken(self, token: Token, replace_existing: bool = True) -> None:
|
def saveAccessToken(self, token: Token, replace_existing: bool = True) -> None:
|
||||||
|
|
@ -1908,6 +2593,56 @@ class AppObjects:
|
||||||
)
|
)
|
||||||
return None
|
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(
|
def findActiveTokenById(
|
||||||
self,
|
self,
|
||||||
tokenId: str,
|
tokenId: str,
|
||||||
|
|
@ -2340,6 +3075,42 @@ class AppObjects:
|
||||||
logger.error(f"Error getting role by label {roleLabel}: {str(e)}")
|
logger.error(f"Error getting role by label {roleLabel}: {str(e)}")
|
||||||
return None
|
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]:
|
def getAllRoles(self, pagination: Optional[PaginationParams] = None) -> Union[List[Role], PaginatedResult]:
|
||||||
"""
|
"""
|
||||||
Get all roles with optional pagination, sorting, and filtering.
|
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]:
|
def _getUserRolesInInstance(rootInterface, userId: str, instanceId: str) -> List[str]:
|
||||||
"""Get all role labels for a user in a feature instance."""
|
"""Get all role labels for a user in a feature instance."""
|
||||||
try:
|
try:
|
||||||
from modules.datamodels.datamodelRbac import Role
|
# Get FeatureAccess for this user and instance (Pydantic model)
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
|
||||||
|
|
||||||
# Get FeatureAccess for this user and instance
|
if featureAccess:
|
||||||
featureAccesses = rootInterface.db.getRecordset(
|
# Get role IDs via interface method
|
||||||
FeatureAccess,
|
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
|
||||||
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if featureAccesses:
|
|
||||||
featureAccessId = featureAccesses[0].get("id")
|
|
||||||
|
|
||||||
# Get role IDs via FeatureAccessRole junction table
|
if roleIds:
|
||||||
featureAccessRoles = rootInterface.db.getRecordset(
|
# Get ALL roles and extract labels
|
||||||
FeatureAccessRole,
|
|
||||||
recordFilter={"featureAccessId": featureAccessId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if featureAccessRoles:
|
|
||||||
# Get ALL roles, not just the first one
|
|
||||||
roleLabels = []
|
roleLabels = []
|
||||||
for far in featureAccessRoles:
|
for roleId in roleIds:
|
||||||
roleId = far.get("roleId")
|
role = rootInterface.getRole(roleId)
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
if role:
|
||||||
if roles:
|
roleLabels.append(role.roleLabel)
|
||||||
roleLabels.append(roles[0].get("roleLabel", "user"))
|
|
||||||
return roleLabels if roleLabels else ["user"]
|
return roleLabels if roleLabels else ["user"]
|
||||||
|
|
||||||
return ["user"] # Default
|
return ["user"] # Default - no access means basic user level
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error getting user roles: {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]:
|
def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict[str, Any]:
|
||||||
|
|
@ -249,66 +237,53 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext, Role
|
from modules.datamodels.datamodelRbac import AccessRuleContext
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
|
||||||
|
|
||||||
# Get FeatureAccess for this user and instance
|
# Get FeatureAccess for this user and instance (Pydantic model)
|
||||||
featureAccesses = rootInterface.db.getRecordset(
|
featureAccess = rootInterface.getFeatureAccess(userId, instanceId)
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": userId, "featureInstanceId": 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}")
|
logger.debug(f"_getInstancePermissions: No FeatureAccess found for user {userId} and instance {instanceId}")
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
# Get role IDs via FeatureAccessRole junction table
|
# Get role IDs via interface method
|
||||||
featureAccessId = featureAccesses[0].get("id")
|
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
|
||||||
featureAccessRoles = rootInterface.db.getRecordset(
|
|
||||||
FeatureAccessRole,
|
|
||||||
recordFilter={"featureAccessId": featureAccessId}
|
|
||||||
)
|
|
||||||
roleIds = [far.get("roleId") for far in featureAccessRoles]
|
|
||||||
|
|
||||||
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccessId}, roleIds={roleIds}")
|
logger.debug(f"_getInstancePermissions: featureAccessId={featureAccess.id}, roleIds={roleIds}")
|
||||||
|
|
||||||
if not 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
|
return permissions
|
||||||
|
|
||||||
# Check if user has admin role
|
# Check if user has admin role
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roles:
|
if role and "admin" in role.roleLabel.lower():
|
||||||
roleLabel = roles[0].get("roleLabel", "").lower()
|
permissions["isAdmin"] = True
|
||||||
if "admin" in roleLabel:
|
break
|
||||||
permissions["isAdmin"] = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# Get permissions (AccessRules) for all roles
|
# Get permissions (AccessRules) for all roles
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
accessRules = rootInterface.db.getRecordset(
|
# Get all rules for this role (returns Pydantic models)
|
||||||
AccessRule,
|
accessRules = rootInterface.getAccessRules(roleId=roleId)
|
||||||
recordFilter={"roleId": roleId}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
|
logger.debug(f"_getInstancePermissions: roleId={roleId}, accessRules={len(accessRules) if accessRules else 0}")
|
||||||
|
|
||||||
for rule in accessRules:
|
for rule in accessRules:
|
||||||
context = rule.get("context", "")
|
context = rule.context
|
||||||
item = rule.get("item", "")
|
item = rule.item or ""
|
||||||
|
|
||||||
# Handle DATA context (tables/fields)
|
# Handle DATA context (tables/fields)
|
||||||
if context == "DATA" or context == AccessRuleContext.DATA:
|
if context == AccessRuleContext.DATA or context == "DATA":
|
||||||
if item:
|
if item:
|
||||||
# Check if it's a field (table.field) or table
|
# Check if it's a field (table.field) or table
|
||||||
if "." in item:
|
if "." in item:
|
||||||
tableName, fieldName = item.split(".", 1)
|
tableName, fieldName = item.split(".", 1)
|
||||||
if fieldName not in permissions["fields"]:
|
if fieldName not in permissions["fields"]:
|
||||||
permissions["fields"][fieldName] = {"view": False}
|
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:
|
else:
|
||||||
tableName = item
|
tableName = item
|
||||||
if tableName not in permissions["tables"]:
|
if tableName not in permissions["tables"]:
|
||||||
|
|
@ -322,20 +297,18 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
|
|
||||||
# Merge permissions (highest wins)
|
# Merge permissions (highest wins)
|
||||||
current = permissions["tables"][tableName]
|
current = permissions["tables"][tableName]
|
||||||
current["view"] = current["view"] or rule.get("view", False)
|
current["view"] = current["view"] or rule.view
|
||||||
current["read"] = _mergeAccessLevel(current["read"], rule.get("read") or "n")
|
current["read"] = _mergeAccessLevel(current["read"], rule.read or "n")
|
||||||
current["create"] = _mergeAccessLevel(current["create"], rule.get("create") or "n")
|
current["create"] = _mergeAccessLevel(current["create"], rule.create or "n")
|
||||||
current["update"] = _mergeAccessLevel(current["update"], rule.get("update") or "n")
|
current["update"] = _mergeAccessLevel(current["update"], rule.update or "n")
|
||||||
current["delete"] = _mergeAccessLevel(current["delete"], rule.get("delete") or "n")
|
current["delete"] = _mergeAccessLevel(current["delete"], rule.delete or "n")
|
||||||
|
|
||||||
# Handle UI context (views)
|
# Handle UI context (views)
|
||||||
# Views are stored with full objectKey (e.g., ui.feature.trustee.dashboard)
|
elif context == AccessRuleContext.UI or context == "UI":
|
||||||
elif context == "UI" or context == AccessRuleContext.UI:
|
|
||||||
ruleView = rule.get("view", False)
|
|
||||||
if item:
|
if item:
|
||||||
# Store with full objectKey as per Navigation-API-Konzept
|
# Store with full objectKey as per Navigation-API-Konzept
|
||||||
permissions["views"][item] = permissions["views"].get(item, False) or ruleView
|
permissions["views"][item] = permissions["views"].get(item, False) or rule.view
|
||||||
elif ruleView:
|
elif rule.view:
|
||||||
# item=None means all views - set a wildcard flag
|
# item=None means all views - set a wildcard flag
|
||||||
permissions["views"]["_all"] = True
|
permissions["views"]["_all"] = True
|
||||||
|
|
||||||
|
|
@ -343,7 +316,7 @@ def _getInstancePermissions(rootInterface, userId: str, instanceId: str) -> Dict
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error getting instance permissions: {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:
|
def _mergeAccessLevel(current: str, new: str) -> str:
|
||||||
|
|
@ -924,49 +897,35 @@ async def list_feature_instance_users(
|
||||||
detail="Access denied to this feature instance"
|
detail="Access denied to this feature instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all FeatureAccess records for this instance
|
# Get all FeatureAccess records for this instance (Pydantic models)
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
|
||||||
from modules.datamodels.datamodelRbac import Role
|
|
||||||
|
|
||||||
featureAccesses = rootInterface.db.getRecordset(
|
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for fa in featureAccesses:
|
for fa in featureAccesses:
|
||||||
userId = fa.get("userId")
|
# Get user info (Pydantic model)
|
||||||
featureAccessId = fa.get("id")
|
user = rootInterface.getUser(str(fa.userId))
|
||||||
|
if not user:
|
||||||
# Get user info
|
|
||||||
users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": userId})
|
|
||||||
if not users:
|
|
||||||
continue
|
continue
|
||||||
user = users[0]
|
|
||||||
|
|
||||||
# Get role IDs via FeatureAccessRole junction table
|
# Get role IDs via interface method
|
||||||
featureAccessRoles = rootInterface.db.getRecordset(
|
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(fa.id))
|
||||||
FeatureAccessRole,
|
|
||||||
recordFilter={"featureAccessId": featureAccessId}
|
|
||||||
)
|
|
||||||
roleIds = [far.get("roleId") for far in featureAccessRoles]
|
|
||||||
|
|
||||||
# Get role labels
|
# Get role labels
|
||||||
roleLabels = []
|
roleLabels = []
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roles:
|
if role:
|
||||||
roleLabels.append(roles[0].get("roleLabel", ""))
|
roleLabels.append(role.roleLabel)
|
||||||
|
|
||||||
result.append(FeatureInstanceUserResponse(
|
result.append(FeatureInstanceUserResponse(
|
||||||
id=featureAccessId, # FeatureAccess ID as primary key
|
id=str(fa.id), # FeatureAccess ID as primary key
|
||||||
userId=userId,
|
userId=str(fa.userId),
|
||||||
username=user.get("username", ""),
|
username=user.username,
|
||||||
email=user.get("email"),
|
email=user.email,
|
||||||
fullName=user.get("fullName"),
|
fullName=user.fullName,
|
||||||
roleIds=roleIds,
|
roleIds=roleIds,
|
||||||
roleLabels=roleLabels,
|
roleLabels=roleLabels,
|
||||||
enabled=fa.get("enabled", True)
|
enabled=fa.enabled
|
||||||
))
|
))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -1026,8 +985,8 @@ async def add_user_to_feature_instance(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify user exists
|
# Verify user exists
|
||||||
users = rootInterface.db.getRecordset(UserInDB, recordFilter={"id": data.userId})
|
user = rootInterface.getUser(data.userId)
|
||||||
if not users:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"User '{data.userId}' 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
|
# Check if user already has access
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
||||||
existingAccess = rootInterface.db.getRecordset(
|
existingAccess = rootInterface.getFeatureAccess(data.userId, instanceId)
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": data.userId, "featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
if existingAccess:
|
if existingAccess:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
|
@ -1131,17 +1087,14 @@ async def remove_user_from_feature_instance(
|
||||||
|
|
||||||
# Find FeatureAccess record
|
# Find FeatureAccess record
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess
|
from modules.datamodels.datamodelMembership import FeatureAccess
|
||||||
existingAccess = rootInterface.db.getRecordset(
|
existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
if not existingAccess:
|
if not existingAccess:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="User does not have access to this feature instance"
|
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)
|
# Delete FeatureAccess (CASCADE will delete FeatureAccessRole records)
|
||||||
rootInterface.db.recordDelete(FeatureAccess, featureAccessId)
|
rootInterface.db.recordDelete(FeatureAccess, featureAccessId)
|
||||||
|
|
@ -1215,29 +1168,21 @@ async def update_feature_instance_user_roles(
|
||||||
|
|
||||||
# Find FeatureAccess record
|
# Find FeatureAccess record
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
from modules.datamodels.datamodelMembership import FeatureAccess, FeatureAccessRole
|
||||||
existingAccess = rootInterface.db.getRecordset(
|
existingAccess = rootInterface.getFeatureAccess(userId, instanceId)
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
if not existingAccess:
|
if not existingAccess:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="User does not have access to this feature instance"
|
detail="User does not have access to this feature instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
featureAccessId = existingAccess[0].get("id")
|
featureAccessId = str(existingAccess.id)
|
||||||
|
|
||||||
# Update enabled flag if provided
|
# Update enabled flag if provided
|
||||||
if data.enabled is not None:
|
if data.enabled is not None:
|
||||||
rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled})
|
rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled})
|
||||||
|
|
||||||
# Delete existing FeatureAccessRole records
|
# Delete existing FeatureAccessRole records via interface method
|
||||||
existingRoles = rootInterface.db.getRecordset(
|
rootInterface.deleteFeatureAccessRoles(featureAccessId)
|
||||||
FeatureAccessRole,
|
|
||||||
recordFilter={"featureAccessId": featureAccessId}
|
|
||||||
)
|
|
||||||
for role in existingRoles:
|
|
||||||
rootInterface.db.recordDelete(FeatureAccessRole, role.get("id"))
|
|
||||||
|
|
||||||
# Create new FeatureAccessRole records
|
# Create new FeatureAccessRole records
|
||||||
for roleId in data.roleIds:
|
for roleId in data.roleIds:
|
||||||
|
|
@ -1304,21 +1249,17 @@ async def get_feature_instance_available_roles(
|
||||||
detail="Access denied to this feature instance"
|
detail="Access denied to this feature instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get roles for this instance
|
# Get roles for this instance using interface method
|
||||||
from modules.datamodels.datamodelRbac import Role
|
instanceRoles = rootInterface.getRolesByFeatureInstance(instanceId)
|
||||||
instanceRoles = rootInterface.db.getRecordset(
|
|
||||||
Role,
|
|
||||||
recordFilter={"featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for role in instanceRoles:
|
for role in instanceRoles:
|
||||||
result.append({
|
result.append({
|
||||||
"id": role.get("id"),
|
"id": role.id,
|
||||||
"roleLabel": role.get("roleLabel"),
|
"roleLabel": role.roleLabel,
|
||||||
"description": role.get("description", {}),
|
"description": role.description or {},
|
||||||
"featureCode": role.get("featureCode"),
|
"featureCode": role.featureCode,
|
||||||
"isSystemRole": role.get("isSystemRole", False)
|
"isSystemRole": role.isSystemRole
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -1394,15 +1335,13 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
|
||||||
# Check if any of the user's roles is an admin role
|
# Check if any of the user's roles is an admin role
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
from modules.datamodels.datamodelRbac import Role
|
|
||||||
|
|
||||||
for roleId in context.roleIds:
|
for roleId in context.roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
roleLabel = role.roleLabel
|
||||||
roleLabel = role.get("roleLabel", "")
|
|
||||||
# Admin role at mandate level (not feature-instance level)
|
# 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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -85,34 +85,31 @@ async def export_global_rbac(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get all global template roles (mandateId is NULL)
|
# Get all global template roles (mandateId is NULL) using interface method
|
||||||
allRoles = rootInterface.db.getRecordset(Role)
|
allRoles = rootInterface.getAllRoles()
|
||||||
globalRoles = [r for r in allRoles if r.get("mandateId") is None]
|
globalRoles = [r for r in allRoles if r.mandateId is None]
|
||||||
|
|
||||||
exportRoles = []
|
exportRoles = []
|
||||||
for role in globalRoles:
|
for role in globalRoles:
|
||||||
roleId = role.get("id")
|
roleId = role.id
|
||||||
|
|
||||||
# Get access rules for this role
|
# Get access rules for this role using interface method
|
||||||
accessRules = rootInterface.db.getRecordset(
|
accessRules = rootInterface.getAccessRulesByRole(roleId)
|
||||||
AccessRule,
|
|
||||||
recordFilter={"roleId": roleId}
|
|
||||||
)
|
|
||||||
|
|
||||||
exportRoles.append(RoleExport(
|
exportRoles.append(RoleExport(
|
||||||
roleLabel=role.get("roleLabel"),
|
roleLabel=role.roleLabel,
|
||||||
description=role.get("description", {}),
|
description=role.description or {},
|
||||||
featureCode=role.get("featureCode"),
|
featureCode=role.featureCode,
|
||||||
isSystemRole=role.get("isSystemRole", False),
|
isSystemRole=role.isSystemRole,
|
||||||
accessRules=[
|
accessRules=[
|
||||||
{
|
{
|
||||||
"context": r.get("context"),
|
"context": r.context,
|
||||||
"item": r.get("item"),
|
"item": r.item,
|
||||||
"view": r.get("view", False),
|
"view": r.view if r.view is not None else False,
|
||||||
"read": r.get("read"),
|
"read": r.read,
|
||||||
"create": r.get("create"),
|
"create": r.create,
|
||||||
"update": r.get("update"),
|
"update": r.update,
|
||||||
"delete": r.get("delete")
|
"delete": r.delete
|
||||||
}
|
}
|
||||||
for r in accessRules
|
for r in accessRules
|
||||||
]
|
]
|
||||||
|
|
@ -191,21 +188,20 @@ async def import_global_rbac(
|
||||||
result.rolesSkipped += 1
|
result.rolesSkipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if role exists (global role with same label and featureCode)
|
# Check if role exists (global role with same label and featureCode) using interface method
|
||||||
existingRoles = rootInterface.db.getRecordset(
|
allRoles = rootInterface.getAllRoles()
|
||||||
Role,
|
existingRoles = [
|
||||||
recordFilter={
|
r for r in allRoles
|
||||||
"roleLabel": roleLabel,
|
if r.roleLabel == roleLabel
|
||||||
"mandateId": None,
|
and r.mandateId is None
|
||||||
"featureCode": featureCode
|
and r.featureCode == featureCode
|
||||||
}
|
]
|
||||||
)
|
|
||||||
|
|
||||||
if existingRoles:
|
if existingRoles:
|
||||||
if updateExisting:
|
if updateExisting:
|
||||||
# Update existing role
|
# Update existing role
|
||||||
existingRole = existingRoles[0]
|
existingRole = existingRoles[0]
|
||||||
roleId = existingRole.get("id")
|
roleId = existingRole.id
|
||||||
|
|
||||||
rootInterface.db.recordModify(
|
rootInterface.db.recordModify(
|
||||||
Role,
|
Role,
|
||||||
|
|
@ -315,41 +311,38 @@ async def export_mandate_rbac(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get mandate-level roles
|
# Get mandate-level roles using interface method
|
||||||
allRoles = rootInterface.db.getRecordset(Role)
|
allRoles = rootInterface.getAllRoles()
|
||||||
mandateRoles = [
|
mandateRoles = [
|
||||||
r for r in allRoles
|
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
|
# Filter by feature instance if not including them
|
||||||
if not includeFeatureInstances:
|
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 = []
|
exportRoles = []
|
||||||
for role in mandateRoles:
|
for role in mandateRoles:
|
||||||
roleId = role.get("id")
|
roleId = role.id
|
||||||
|
|
||||||
# Get access rules for this role
|
# Get access rules for this role using interface method
|
||||||
accessRules = rootInterface.db.getRecordset(
|
accessRules = rootInterface.getAccessRulesByRole(roleId)
|
||||||
AccessRule,
|
|
||||||
recordFilter={"roleId": roleId}
|
|
||||||
)
|
|
||||||
|
|
||||||
exportRoles.append(RoleExport(
|
exportRoles.append(RoleExport(
|
||||||
roleLabel=role.get("roleLabel"),
|
roleLabel=role.roleLabel,
|
||||||
description=role.get("description", {}),
|
description=role.description or {},
|
||||||
featureCode=role.get("featureCode"),
|
featureCode=role.featureCode,
|
||||||
isSystemRole=role.get("isSystemRole", False),
|
isSystemRole=role.isSystemRole,
|
||||||
accessRules=[
|
accessRules=[
|
||||||
{
|
{
|
||||||
"context": r.get("context"),
|
"context": r.context,
|
||||||
"item": r.get("item"),
|
"item": r.item,
|
||||||
"view": r.get("view", False),
|
"view": r.view if r.view is not None else False,
|
||||||
"read": r.get("read"),
|
"read": r.read,
|
||||||
"create": r.get("create"),
|
"create": r.create,
|
||||||
"update": r.get("update"),
|
"update": r.update,
|
||||||
"delete": r.get("delete")
|
"delete": r.delete
|
||||||
}
|
}
|
||||||
for r in accessRules
|
for r in accessRules
|
||||||
]
|
]
|
||||||
|
|
@ -453,21 +446,20 @@ async def import_mandate_rbac(
|
||||||
result.rolesSkipped += 1
|
result.rolesSkipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if role exists (mandate role with same label)
|
# Check if role exists (mandate role with same label) using interface method
|
||||||
existingRoles = rootInterface.db.getRecordset(
|
allRoles = rootInterface.getAllRoles()
|
||||||
Role,
|
existingRoles = [
|
||||||
recordFilter={
|
r for r in allRoles
|
||||||
"roleLabel": roleLabel,
|
if r.roleLabel == roleLabel
|
||||||
"mandateId": str(context.mandateId),
|
and str(r.mandateId) == str(context.mandateId)
|
||||||
"featureInstanceId": None # Only mandate-level roles
|
and r.featureInstanceId is None # Only mandate-level roles
|
||||||
}
|
]
|
||||||
)
|
|
||||||
|
|
||||||
if existingRoles:
|
if existingRoles:
|
||||||
if updateExisting:
|
if updateExisting:
|
||||||
# Update existing role
|
# Update existing role
|
||||||
existingRole = existingRoles[0]
|
existingRole = existingRoles[0]
|
||||||
roleId = existingRole.get("id")
|
roleId = existingRole.id
|
||||||
|
|
||||||
rootInterface.db.recordModify(
|
rootInterface.db.recordModify(
|
||||||
Role,
|
Role,
|
||||||
|
|
@ -556,12 +548,11 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
for roleId in context.roleIds:
|
for roleId in context.roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
roleLabel = role.roleLabel
|
||||||
roleLabel = role.get("roleLabel", "")
|
|
||||||
# Admin role at mandate level
|
# 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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
@ -580,10 +571,10 @@ def _updateAccessRules(interface, roleId: str, newRules: List[Dict[str, Any]]) -
|
||||||
Number of rules created/updated
|
Number of rules created/updated
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Delete existing rules for this role
|
# Delete existing rules for this role using interface method
|
||||||
existingRules = interface.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
existingRules = interface.getAccessRulesByRole(roleId)
|
||||||
for rule in existingRules:
|
for rule in existingRules:
|
||||||
interface.db.recordDelete(AccessRule, rule.get("id"))
|
interface.db.recordDelete(AccessRule, rule.id)
|
||||||
|
|
||||||
# Create new rules
|
# Create new rules
|
||||||
count = 0
|
count = 0
|
||||||
|
|
|
||||||
|
|
@ -36,25 +36,17 @@ def _getUserRoleLabels(interface, userId: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
roleLabels: Set[str] = set()
|
roleLabels: Set[str] = set()
|
||||||
|
|
||||||
# Get all UserMandate records for this user
|
# Get all UserMandate records for this user (Pydantic models)
|
||||||
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
|
userMandates = interface.getUserMandates(userId)
|
||||||
|
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
userMandateId = um.get("id")
|
# Get all UserMandateRole records for this membership (Pydantic models)
|
||||||
if not userMandateId:
|
userMandateRoles = interface.getUserMandateRoles(str(um.id))
|
||||||
continue
|
|
||||||
|
|
||||||
# Get all UserMandateRole records for this membership
|
|
||||||
userMandateRoles = interface.db.getRecordset(
|
|
||||||
UserMandateRole,
|
|
||||||
recordFilter={"userMandateId": str(userMandateId)}
|
|
||||||
)
|
|
||||||
|
|
||||||
for umr in userMandateRoles:
|
for umr in userMandateRoles:
|
||||||
roleId = umr.get("roleId")
|
if umr.roleId:
|
||||||
if roleId:
|
|
||||||
# Get role by ID to get roleLabel
|
# Get role by ID to get roleLabel
|
||||||
role = interface.getRole(str(roleId))
|
role = interface.getRole(str(umr.roleId))
|
||||||
if role:
|
if role:
|
||||||
roleLabels.add(role.roleLabel)
|
roleLabels.add(role.roleLabel)
|
||||||
|
|
||||||
|
|
@ -362,21 +354,13 @@ async def list_users_with_roles(
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
# Get all users (SysAdmin sees all)
|
# Get all users via interface method (Pydantic models)
|
||||||
# Use db.getRecordset with UserInDB (the actual database model)
|
users = interface.getAllUsers()
|
||||||
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))
|
|
||||||
|
|
||||||
# Filter by mandate if specified (via UserMandate table)
|
# Filter by mandate if specified (via UserMandate table)
|
||||||
if mandateId:
|
if mandateId:
|
||||||
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
|
userMandates = interface.getUserMandatesByMandate(mandateId)
|
||||||
mandateUserIds = {str(um["userId"]) for um in userMandates}
|
mandateUserIds = {str(um.userId) for um in userMandates}
|
||||||
users = [u for u in users if str(u.id) in mandateUserIds]
|
users = [u for u in users if str(u.id) in mandateUserIds]
|
||||||
|
|
||||||
# Filter by role if specified (via UserMandateRole)
|
# 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}")
|
logger.warning(f"Non-standard role label assigned: {roleLabel}")
|
||||||
|
|
||||||
# Get user's first mandate (for role assignment)
|
# Get user's first mandate (for role assignment)
|
||||||
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
|
userMandates = interface.getUserMandates(userId)
|
||||||
if not userMandates:
|
if not userMandates:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"User {userId} has no mandate memberships. Add to mandate first."
|
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
|
# Get current roles for this mandate (Pydantic models)
|
||||||
existingRoles = interface.db.getRecordset(
|
existingRoles = interface.getUserMandateRoles(userMandateId)
|
||||||
UserMandateRole,
|
existingRoleIds = {str(r.roleId) for r in existingRoles}
|
||||||
recordFilter={"userMandateId": userMandateId}
|
|
||||||
)
|
|
||||||
existingRoleIds = {str(r.get("roleId")) for r in existingRoles}
|
|
||||||
|
|
||||||
# Convert roleLabels to roleIds
|
# Convert roleLabels to roleIds
|
||||||
newRoleIds = set()
|
newRoleIds = set()
|
||||||
|
|
@ -524,8 +505,8 @@ async def update_user_roles(
|
||||||
|
|
||||||
# Remove roles that are no longer needed
|
# Remove roles that are no longer needed
|
||||||
for existingRole in existingRoles:
|
for existingRole in existingRoles:
|
||||||
if str(existingRole.get("roleId")) not in newRoleIds:
|
if str(existingRole.roleId) not in newRoleIds:
|
||||||
interface.db.recordDelete(UserMandateRole, str(existingRole.get("id")))
|
interface.removeRoleFromUserMandate(userMandateId, str(existingRole.roleId))
|
||||||
|
|
||||||
# Add new roles
|
# Add new roles
|
||||||
for roleId in newRoleIds:
|
for roleId in newRoleIds:
|
||||||
|
|
@ -596,25 +577,22 @@ async def add_user_role(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get user's first mandate
|
# Get user's first mandate
|
||||||
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
|
userMandates = interface.getUserMandates(userId)
|
||||||
if not userMandates:
|
if not userMandates:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"User {userId} has no mandate memberships. Add to mandate first."
|
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
|
# Check if role is already assigned - use interface method
|
||||||
existingAssignment = interface.db.getRecordset(
|
existingRoles = interface.getUserMandateRoles(userMandateId)
|
||||||
UserMandateRole,
|
roleAlreadyAssigned = any(str(r.roleId) == str(role.id) for r in existingRoles)
|
||||||
recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not existingAssignment:
|
if not roleAlreadyAssigned:
|
||||||
# Add the role
|
# Add the role via interface method
|
||||||
newRole = UserMandateRole(userMandateId=userMandateId, roleId=str(role.id))
|
interface.addRoleToUserMandate(userMandateId, str(role.id))
|
||||||
interface.db.recordCreate(UserMandateRole, newRole.model_dump())
|
|
||||||
logger.info(f"Added role {roleLabel} to user {userId} by SysAdmin {currentUser.id}")
|
logger.info(f"Added role {roleLabel} to user {userId} by SysAdmin {currentUser.id}")
|
||||||
|
|
||||||
userRoleLabels = _getUserRoleLabels(interface, userId)
|
userRoleLabels = _getUserRoleLabels(interface, userId)
|
||||||
|
|
@ -678,20 +656,14 @@ async def remove_user_role(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove role from all user's mandates
|
# Remove role from all user's mandates
|
||||||
userMandates = interface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
|
userMandates = interface.getUserMandates(userId)
|
||||||
roleRemoved = False
|
roleRemoved = False
|
||||||
|
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
userMandateId = str(um.get("id"))
|
userMandateId = str(um.id)
|
||||||
|
|
||||||
# Find and delete the role assignment
|
# Remove role via interface method
|
||||||
assignments = interface.db.getRecordset(
|
if interface.removeRoleFromUserMandate(userMandateId, str(role.id)):
|
||||||
UserMandateRole,
|
|
||||||
recordFilter={"userMandateId": userMandateId, "roleId": str(role.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
for assignment in assignments:
|
|
||||||
interface.db.recordDelete(UserMandateRole, str(assignment.get("id")))
|
|
||||||
roleRemoved = True
|
roleRemoved = True
|
||||||
|
|
||||||
if roleRemoved:
|
if roleRemoved:
|
||||||
|
|
@ -751,25 +723,21 @@ async def get_users_with_role(
|
||||||
detail=f"Role '{roleLabel}' not found"
|
detail=f"Role '{roleLabel}' not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all UserMandateRole assignments for this role
|
# Get all UserMandateRole assignments for this role (Pydantic models)
|
||||||
roleAssignments = interface.db.getRecordset(
|
roleAssignments = interface.getUserMandateRolesByRole(str(role.id))
|
||||||
UserMandateRole,
|
|
||||||
recordFilter={"roleId": str(role.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get unique userMandateIds
|
# 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
|
# Get userIds from UserMandate records
|
||||||
userIds: Set[str] = set()
|
userIds: Set[str] = set()
|
||||||
for userMandateId in userMandateIds:
|
for userMandateId in userMandateIds:
|
||||||
umRecords = interface.db.getRecordset(UserMandate, recordFilter={"id": userMandateId})
|
um = interface.getUserMandateById(userMandateId)
|
||||||
if umRecords:
|
if um:
|
||||||
um = umRecords[0]
|
|
||||||
# Filter by mandate if specified
|
# Filter by mandate if specified
|
||||||
if mandateId and str(um.get("mandateId")) != mandateId:
|
if mandateId and str(um.mandateId) != mandateId:
|
||||||
continue
|
continue
|
||||||
userIds.add(str(um.get("userId")))
|
userIds.add(str(um.userId))
|
||||||
|
|
||||||
# Get users and format response
|
# Get users and format response
|
||||||
result = []
|
result = []
|
||||||
|
|
|
||||||
|
|
@ -179,17 +179,15 @@ async def get_all_permissions(
|
||||||
|
|
||||||
# For UI/RESOURCE: Load system roles the user has across ALL their mandates
|
# 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
|
# This allows users to access system UI elements without needing a specific mandate header
|
||||||
userMandates = rootInterface.db.getRecordset(
|
allUserMandates = rootInterface.getUserMandates(str(reqContext.user.id))
|
||||||
UserMandate,
|
userMandates = [um for um in allUserMandates if um.enabled]
|
||||||
recordFilter={"userId": str(reqContext.user.id), "enabled": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"UI/RESOURCE permissions: Found {len(userMandates)} UserMandates for user {reqContext.user.id}")
|
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
|
# Collect all role IDs the user has across all mandates
|
||||||
for userMandate in userMandates:
|
for userMandate in userMandates:
|
||||||
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.get("id"))
|
mandateRoleIds = rootInterface.getRoleIdsForUserMandate(userMandate.id)
|
||||||
logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.get('id')} (mandate {userMandate.get('mandateId')}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
|
logger.debug(f"UI/RESOURCE permissions: UserMandate {userMandate.id} (mandate {userMandate.mandateId}) has {len(mandateRoleIds)} roles: {mandateRoleIds}")
|
||||||
for rid in mandateRoleIds:
|
for rid in mandateRoleIds:
|
||||||
if rid not in roleIds:
|
if rid not in roleIds:
|
||||||
roleIds.append(rid)
|
roleIds.append(rid)
|
||||||
|
|
@ -210,14 +208,11 @@ async def get_all_permissions(
|
||||||
allRules[ctx] = []
|
allRules[ctx] = []
|
||||||
# Get all rules for user's roles - bypass RBAC filtering
|
# Get all rules for user's roles - bypass RBAC filtering
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
ruleRecords = rootInterface.db.getRecordset(
|
# Use interface method and filter by context
|
||||||
AccessRule,
|
rules = rootInterface.getAccessRulesByRole(str(roleId))
|
||||||
recordFilter={"roleId": str(roleId), "context": ctx.value}
|
for rule in rules:
|
||||||
)
|
if rule.context == ctx.value:
|
||||||
for ruleRecord in ruleRecords:
|
allRules[ctx].append(rule)
|
||||||
# Convert dict to AccessRule object
|
|
||||||
cleanedRule = {k: v for k, v in ruleRecord.items() if not k.startswith("_")}
|
|
||||||
allRules[ctx].append(AccessRule(**cleanedRule))
|
|
||||||
|
|
||||||
# Build result: for each context, collect all unique items and calculate permissions
|
# Build result: for each context, collect all unique items and calculate permissions
|
||||||
for ctx in contextsToFetch:
|
for ctx in contextsToFetch:
|
||||||
|
|
@ -405,14 +400,8 @@ async def get_access_rules_by_role(
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
# Build filter for roleId
|
# Get rules from database using interface method
|
||||||
recordFilter = {"roleId": roleId}
|
ruleObjects = interface.getAccessRulesByRole(roleId)
|
||||||
|
|
||||||
# Get rules from database
|
|
||||||
rules = interface.db.getRecordset(AccessRule, recordFilter=recordFilter)
|
|
||||||
|
|
||||||
# Convert to AccessRule objects
|
|
||||||
ruleObjects = [AccessRule(**rule) for rule in rules]
|
|
||||||
|
|
||||||
return PaginatedResponse(
|
return PaginatedResponse(
|
||||||
items=[rule.model_dump() for rule in ruleObjects],
|
items=[rule.model_dump() for rule in ruleObjects],
|
||||||
|
|
@ -1128,13 +1117,9 @@ async def getCatalogObjects(
|
||||||
if mandateId:
|
if mandateId:
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
# Get all feature instances for this mandate
|
# Get all feature instances for this mandate using interface method
|
||||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
instances = interface.getFeatureInstancesByMandate(mandateId, enabledOnly=True)
|
||||||
instances = interface.db.getRecordset(
|
activeFeatures = set(inst.featureCode for inst in instances)
|
||||||
FeatureInstance,
|
|
||||||
recordFilter={"mandateId": mandateId, "enabled": True}
|
|
||||||
)
|
|
||||||
activeFeatures = set(inst.get("featureCode") for inst in instances)
|
|
||||||
# Always include "system" feature
|
# Always include "system" feature
|
||||||
activeFeatures.add("system")
|
activeFeatures.add("system")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,15 @@ def _getAccessLevelLabel(level: Optional[str]) -> str:
|
||||||
return labels.get(level, "-")
|
return labels.get(level, "-")
|
||||||
|
|
||||||
|
|
||||||
def _getRoleScope(role: Dict[str, Any]) -> str:
|
def _getRoleScope(role) -> str:
|
||||||
"""Determine the scope of a role."""
|
"""Determine the scope of a role. Accepts Role object or dict."""
|
||||||
if role.get("featureInstanceId"):
|
# 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"
|
return "instance"
|
||||||
elif role.get("mandateId"):
|
elif mandateId:
|
||||||
return "mandate"
|
return "mandate"
|
||||||
else:
|
else:
|
||||||
return "global"
|
return "global"
|
||||||
|
|
@ -79,18 +83,18 @@ async def listUsersForOverview(
|
||||||
try:
|
try:
|
||||||
interface = getRootInterface()
|
interface = getRootInterface()
|
||||||
|
|
||||||
# Get all users
|
# Get all users using interface method
|
||||||
allUsersData = interface.db.getRecordset(UserInDB)
|
allUsers = interface.getAllUsers()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for u in allUsersData:
|
for u in allUsers:
|
||||||
result.append({
|
result.append({
|
||||||
"id": u.get("id"),
|
"id": u.id,
|
||||||
"username": u.get("username"),
|
"username": u.username,
|
||||||
"email": u.get("email"),
|
"email": u.email,
|
||||||
"fullName": u.get("fullName"),
|
"fullName": u.fullName,
|
||||||
"isSysAdmin": u.get("isSysAdmin", False),
|
"isSysAdmin": u.isSysAdmin,
|
||||||
"enabled": u.get("enabled", True),
|
"enabled": u.enabled,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort by username
|
# Sort by username
|
||||||
|
|
@ -172,47 +176,43 @@ async def getUserAccessOverview(
|
||||||
allRoles = []
|
allRoles = []
|
||||||
roleIdToInfo = {} # Map roleId to role info for later reference
|
roleIdToInfo = {} # Map roleId to role info for later reference
|
||||||
|
|
||||||
# Get mandates for this user
|
# Get mandates for this user using interface method
|
||||||
mandateFilter = {"userId": userId, "enabled": True}
|
allUserMandates = interface.getUserMandates(userId)
|
||||||
|
# Filter by enabled and optionally mandateId
|
||||||
|
userMandates = [um for um in allUserMandates if um.enabled]
|
||||||
if mandateId:
|
if mandateId:
|
||||||
mandateFilter["mandateId"] = mandateId
|
userMandates = [um for um in userMandates if um.mandateId == mandateId]
|
||||||
|
|
||||||
userMandates = interface.db.getRecordset(UserMandate, recordFilter=mandateFilter)
|
|
||||||
|
|
||||||
mandatesInfo = []
|
mandatesInfo = []
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
umId = um.get("id")
|
umId = um.id
|
||||||
umMandateId = um.get("mandateId")
|
umMandateId = um.mandateId
|
||||||
|
|
||||||
# Get mandate name
|
# Get mandate name
|
||||||
mandate = interface.getMandate(umMandateId)
|
mandate = interface.getMandate(umMandateId)
|
||||||
mandateName = mandate.name if mandate else umMandateId
|
mandateName = mandate.name if mandate else umMandateId
|
||||||
|
|
||||||
# Get roles for this UserMandate
|
# Get roles for this UserMandate using interface method
|
||||||
umRoles = interface.db.getRecordset(
|
umRoles = interface.getUserMandateRoles(umId)
|
||||||
UserMandateRole,
|
|
||||||
recordFilter={"userMandateId": umId}
|
|
||||||
)
|
|
||||||
|
|
||||||
mandateRoleIds = []
|
mandateRoleIds = []
|
||||||
for umr in umRoles:
|
for umr in umRoles:
|
||||||
roleId = umr.get("roleId")
|
roleId = umr.roleId
|
||||||
if roleId:
|
if roleId:
|
||||||
mandateRoleIds.append(roleId)
|
mandateRoleIds.append(roleId)
|
||||||
|
|
||||||
# Get role details
|
# Get role details using interface method
|
||||||
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = interface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
|
||||||
scope = _getRoleScope(role)
|
scope = _getRoleScope(role)
|
||||||
roleInfo = {
|
roleInfo = {
|
||||||
"id": roleId,
|
"id": roleId,
|
||||||
"roleLabel": role.get("roleLabel"),
|
"roleLabel": role.roleLabel,
|
||||||
"description": role.get("description", {}),
|
"description": role.description or {},
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"scopePriority": _getRoleScopePriority(scope),
|
"scopePriority": _getRoleScopePriority(scope),
|
||||||
"mandateId": role.get("mandateId"),
|
"mandateId": role.mandateId,
|
||||||
"featureInstanceId": role.get("featureInstanceId"),
|
"featureInstanceId": role.featureInstanceId,
|
||||||
"source": "mandate",
|
"source": "mandate",
|
||||||
"sourceMandateId": umMandateId,
|
"sourceMandateId": umMandateId,
|
||||||
"sourceMandateName": mandateName,
|
"sourceMandateName": mandateName,
|
||||||
|
|
@ -220,69 +220,59 @@ async def getUserAccessOverview(
|
||||||
allRoles.append(roleInfo)
|
allRoles.append(roleInfo)
|
||||||
roleIdToInfo[roleId] = roleInfo
|
roleIdToInfo[roleId] = roleInfo
|
||||||
|
|
||||||
# Get feature instances for this mandate
|
# Get feature instances for this mandate using interface method
|
||||||
featureInstanceFilter = {"userId": userId, "enabled": True}
|
allFeatureAccesses = interface.getFeatureAccessesForUser(userId)
|
||||||
featureAccesses = interface.db.getRecordset(FeatureAccess, recordFilter=featureInstanceFilter)
|
featureAccesses = [fa for fa in allFeatureAccesses if fa.enabled]
|
||||||
|
|
||||||
featureInstancesInfo = []
|
featureInstancesInfo = []
|
||||||
for fa in featureAccesses:
|
for fa in featureAccesses:
|
||||||
faId = fa.get("id")
|
faId = fa.id
|
||||||
faInstanceId = fa.get("featureInstanceId")
|
faInstanceId = fa.featureInstanceId
|
||||||
|
|
||||||
# Check if instance belongs to this mandate
|
# Check if instance belongs to this mandate using interface method
|
||||||
instance = interface.db.getRecordset(FeatureInstance, recordFilter={"id": faInstanceId})
|
instance = interface.getFeatureInstance(faInstanceId)
|
||||||
if not instance:
|
if not instance:
|
||||||
continue
|
continue
|
||||||
instance = instance[0]
|
|
||||||
|
|
||||||
if instance.get("mandateId") != umMandateId:
|
if instance.mandateId != umMandateId:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Filter by featureInstanceId if specified
|
# Filter by featureInstanceId if specified
|
||||||
if featureInstanceId and faInstanceId != featureInstanceId:
|
if featureInstanceId and faInstanceId != featureInstanceId:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get feature info
|
# Get feature info using interface method
|
||||||
featureCode = instance.get("featureCode")
|
featureCode = instance.featureCode
|
||||||
featureRecords = interface.db.getRecordset(Feature, recordFilter={"code": featureCode})
|
feature = interface.getFeatureByCode(featureCode)
|
||||||
featureLabel = featureRecords[0].get("label", {}) if featureRecords else {}
|
featureLabel = feature.label if feature else {}
|
||||||
|
|
||||||
# Get roles for this FeatureAccess
|
# Get roles for this FeatureAccess using interface method
|
||||||
faRoles = interface.db.getRecordset(
|
instanceRoleIds = interface.getRoleIdsForFeatureAccess(faId)
|
||||||
FeatureAccessRole,
|
|
||||||
recordFilter={"featureAccessId": faId}
|
|
||||||
)
|
|
||||||
|
|
||||||
instanceRoleIds = []
|
for roleId in instanceRoleIds:
|
||||||
for far in faRoles:
|
# Get role details (if not already added)
|
||||||
roleId = far.get("roleId")
|
if roleId not in roleIdToInfo:
|
||||||
if roleId:
|
role = interface.getRole(roleId)
|
||||||
instanceRoleIds.append(roleId)
|
if role:
|
||||||
|
scope = _getRoleScope(role)
|
||||||
# Get role details (if not already added)
|
roleInfo = {
|
||||||
if roleId not in roleIdToInfo:
|
"id": roleId,
|
||||||
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
|
"roleLabel": role.roleLabel,
|
||||||
if roleRecords:
|
"description": role.description or {},
|
||||||
role = roleRecords[0]
|
"scope": scope,
|
||||||
scope = _getRoleScope(role)
|
"scopePriority": _getRoleScopePriority(scope),
|
||||||
roleInfo = {
|
"mandateId": role.mandateId,
|
||||||
"id": roleId,
|
"featureInstanceId": role.featureInstanceId,
|
||||||
"roleLabel": role.get("roleLabel"),
|
"source": "featureInstance",
|
||||||
"description": role.get("description", {}),
|
"sourceInstanceId": faInstanceId,
|
||||||
"scope": scope,
|
"sourceInstanceLabel": instance.label,
|
||||||
"scopePriority": _getRoleScopePriority(scope),
|
}
|
||||||
"mandateId": role.get("mandateId"),
|
allRoles.append(roleInfo)
|
||||||
"featureInstanceId": role.get("featureInstanceId"),
|
roleIdToInfo[roleId] = roleInfo
|
||||||
"source": "featureInstance",
|
|
||||||
"sourceInstanceId": faInstanceId,
|
|
||||||
"sourceInstanceLabel": instance.get("label"),
|
|
||||||
}
|
|
||||||
allRoles.append(roleInfo)
|
|
||||||
roleIdToInfo[roleId] = roleInfo
|
|
||||||
|
|
||||||
featureInstancesInfo.append({
|
featureInstancesInfo.append({
|
||||||
"id": faInstanceId,
|
"id": faInstanceId,
|
||||||
"label": instance.get("label"),
|
"label": instance.label,
|
||||||
"featureCode": featureCode,
|
"featureCode": featureCode,
|
||||||
"featureLabel": featureLabel,
|
"featureLabel": featureLabel,
|
||||||
"roleIds": instanceRoleIds,
|
"roleIds": instanceRoleIds,
|
||||||
|
|
@ -317,12 +307,12 @@ async def getUserAccessOverview(
|
||||||
roleLabel = roleInfo.get("roleLabel", "unknown")
|
roleLabel = roleInfo.get("roleLabel", "unknown")
|
||||||
roleScope = roleInfo.get("scope", "unknown")
|
roleScope = roleInfo.get("scope", "unknown")
|
||||||
|
|
||||||
# Get all rules for this role
|
# Get all rules for this role using interface method
|
||||||
rules = interface.db.getRecordset(AccessRule, recordFilter={"roleId": roleId})
|
rules = interface.getAccessRulesByRole(roleId)
|
||||||
|
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
context = rule.get("context")
|
context = rule.context
|
||||||
item = rule.get("item")
|
item = rule.item
|
||||||
|
|
||||||
accessEntry = {
|
accessEntry = {
|
||||||
"item": item or "(all)",
|
"item": item or "(all)",
|
||||||
|
|
@ -333,20 +323,20 @@ async def getUserAccessOverview(
|
||||||
}
|
}
|
||||||
|
|
||||||
if context == "UI":
|
if context == "UI":
|
||||||
accessEntry["view"] = rule.get("view", False)
|
accessEntry["view"] = rule.view if rule.view is not None else False
|
||||||
if accessEntry["view"]:
|
if accessEntry["view"]:
|
||||||
uiAccess.append(accessEntry)
|
uiAccess.append(accessEntry)
|
||||||
|
|
||||||
elif context == "DATA":
|
elif context == "DATA":
|
||||||
accessEntry["view"] = rule.get("view", False)
|
accessEntry["view"] = rule.view if rule.view is not None else False
|
||||||
accessEntry["read"] = _getAccessLevelLabel(rule.get("read"))
|
accessEntry["read"] = _getAccessLevelLabel(rule.read)
|
||||||
accessEntry["create"] = _getAccessLevelLabel(rule.get("create"))
|
accessEntry["create"] = _getAccessLevelLabel(rule.create)
|
||||||
accessEntry["update"] = _getAccessLevelLabel(rule.get("update"))
|
accessEntry["update"] = _getAccessLevelLabel(rule.update)
|
||||||
accessEntry["delete"] = _getAccessLevelLabel(rule.get("delete"))
|
accessEntry["delete"] = _getAccessLevelLabel(rule.delete)
|
||||||
dataAccess.append(accessEntry)
|
dataAccess.append(accessEntry)
|
||||||
|
|
||||||
elif context == "RESOURCE":
|
elif context == "RESOURCE":
|
||||||
accessEntry["view"] = rule.get("view", False)
|
accessEntry["view"] = rule.view if rule.view is not None else False
|
||||||
if accessEntry["view"]:
|
if accessEntry["view"]:
|
||||||
resourceAccess.append(accessEntry)
|
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
|
- tokenExpiresAt: UTC timestamp or None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Query tokens table for the latest token for this connection
|
# Query tokens table for the latest token for this connection using interface method
|
||||||
tokens = interface.db.getRecordset(
|
latestToken = interface.getConnectionToken(connectionId)
|
||||||
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
|
|
||||||
|
|
||||||
if not latestToken:
|
if not latestToken:
|
||||||
return "none", None
|
return "none", None
|
||||||
|
|
||||||
# Check if token is expired
|
# Check if token is expired
|
||||||
expiresAt = parseTimestamp(latestToken.get("expiresAt"))
|
expiresAt = parseTimestamp(latestToken.expiresAt)
|
||||||
if not expiresAt:
|
if not expiresAt:
|
||||||
return "none", None
|
return "none", None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,9 +291,9 @@ async def delete_mandate(
|
||||||
)
|
)
|
||||||
|
|
||||||
# MULTI-TENANT: Delete all UserMandate entries for this mandate first
|
# 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:
|
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}")
|
logger.info(f"Deleted {len(userMandates)} UserMandate entries for mandate {mandateId}")
|
||||||
|
|
||||||
# Delete mandate
|
# Delete mandate
|
||||||
|
|
@ -377,39 +377,46 @@ async def list_mandate_users(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all UserMandate entries for this mandate
|
# Get all UserMandate entries for this mandate
|
||||||
userMandates = rootInterface.db.getRecordset(
|
userMandates = rootInterface.getUserMandatesByMandate(targetMandateId)
|
||||||
UserMandate,
|
|
||||||
recordFilter={"mandateId": targetMandateId}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
# Get user info
|
# Get user info
|
||||||
user = rootInterface.getUser(um.get("userId"))
|
user = rootInterface.getUser(str(um.userId))
|
||||||
if not user:
|
if not user:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get roles for this membership
|
# 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 = []
|
roleLabels = []
|
||||||
|
filteredRoleIds = []
|
||||||
|
seenLabels = set()
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
role = rootInterface.getRole(roleId)
|
role = rootInterface.getRole(roleId)
|
||||||
if role:
|
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:
|
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({
|
result.append({
|
||||||
"id": um.get("id"), # UserMandate ID as primary key
|
"id": str(um.id), # UserMandate ID as primary key
|
||||||
"userId": str(user.id),
|
"userId": str(user.id),
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
"fullName": user.fullName,
|
"fullName": user.fullName,
|
||||||
"roleIds": roleIds,
|
"roleIds": filteredRoleIds,
|
||||||
"roleLabels": roleLabels,
|
"roleLabels": roleLabels,
|
||||||
"enabled": um.get("enabled", True)
|
"enabled": um.enabled
|
||||||
})
|
})
|
||||||
|
|
||||||
# Apply search, filtering, and sorting if pagination requested
|
# 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)
|
# 6. Validate roles (must exist and belong to this mandate or be global)
|
||||||
for roleId in data.roleIds:
|
for roleId in data.roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
try:
|
||||||
if not roleRecords:
|
rootInterface.validateRoleForMandate(roleId, targetMandateId)
|
||||||
raise HTTPException(
|
except ValueError as e:
|
||||||
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):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=f"Role {roleId} belongs to a different mandate"
|
detail=str(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. Create UserMandate
|
# 7. Create UserMandate
|
||||||
|
|
@ -718,18 +719,12 @@ async def update_user_roles_in_mandate(
|
||||||
|
|
||||||
# Validate new roles
|
# Validate new roles
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
try:
|
||||||
if not roleRecords:
|
rootInterface.validateRoleForMandate(roleId, targetMandateId)
|
||||||
raise HTTPException(
|
except ValueError as e:
|
||||||
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):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
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
|
# 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
|
# Remove existing role assignments
|
||||||
existingRoles = rootInterface.db.getRecordset(
|
rootInterface.deleteUserMandateRoles(str(membership.id))
|
||||||
UserMandateRole,
|
|
||||||
recordFilter={"userMandateId": str(membership.id)}
|
|
||||||
)
|
|
||||||
for er in existingRoles:
|
|
||||||
rootInterface.db.recordDelete(UserMandateRole, er.get("id"))
|
|
||||||
|
|
||||||
# Add new role assignments
|
# Add new role assignments
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
|
|
@ -812,19 +802,17 @@ def _hasMandateAdminRole(context: RequestContext, mandateId: str) -> bool:
|
||||||
rootInterface = interfaceDbApp.getRootInterface()
|
rootInterface = interfaceDbApp.getRootInterface()
|
||||||
|
|
||||||
for roleId in context.roleIds:
|
for roleId in context.roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
|
||||||
roleLabel = role.get("roleLabel", "")
|
|
||||||
# Admin role at mandate level (not feature-instance level)
|
# 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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking mandate admin role: {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:
|
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.
|
Check if excluding this user would leave the mandate without any admins.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get all UserMandates for this mandate
|
# Get all UserMandates for this mandate (Pydantic models)
|
||||||
userMandates = interface.db.getRecordset(
|
allMandates = interface.getUserMandatesByMandate(mandateId)
|
||||||
UserMandate,
|
userMandates = [um for um in allMandates if um.enabled]
|
||||||
recordFilter={"mandateId": mandateId, "enabled": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
adminCount = 0
|
adminCount = 0
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
if str(um.get("userId")) == str(excludeUserId):
|
if str(um.userId) == str(excludeUserId):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if this user has admin role
|
# Check if this user has admin role
|
||||||
roleIds = interface.getRoleIdsForUserMandate(um.get("id"))
|
roleIds = interface.getRoleIdsForUserMandate(str(um.id))
|
||||||
if _hasAdminRoleInList(interface, roleIds, mandateId):
|
if _hasAdminRoleInList(interface, roleIds, mandateId):
|
||||||
adminCount += 1
|
adminCount += 1
|
||||||
|
|
||||||
|
|
@ -852,7 +838,7 @@ def _isLastMandateAdmin(interface, mandateId: str, excludeUserId: str) -> bool:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking last admin: {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:
|
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.
|
Check if any of the role IDs is an admin role for the mandate.
|
||||||
"""
|
"""
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = interface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
# Admin role at mandate level (global or mandate-specific, not feature-instance)
|
||||||
roleLabel = role.get("roleLabel", "")
|
if role.roleLabel == "admin" and not role.featureInstanceId:
|
||||||
roleMandateId = role.get("mandateId")
|
if not role.mandateId or str(role.mandateId) == str(mandateId):
|
||||||
# Admin role at mandate level
|
|
||||||
if roleLabel == "admin" and (not roleMandateId or str(roleMandateId) == str(mandateId)):
|
|
||||||
if not role.get("featureInstanceId"):
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ import modules.interfaces.interfaceDbApp as interfaceDbApp
|
||||||
from modules.auth import limiter, getRequestContext, RequestContext
|
from modules.auth import limiter, getRequestContext, RequestContext
|
||||||
|
|
||||||
# Import the attribute definition and helper functions
|
# 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
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
|
||||||
|
|
||||||
# Configure logger
|
# Configure logger
|
||||||
|
|
@ -251,16 +252,10 @@ async def get_users(
|
||||||
)
|
)
|
||||||
elif context.isSysAdmin:
|
elif context.isSysAdmin:
|
||||||
# SysAdmin without mandateId sees all users
|
# SysAdmin without mandateId sees all users
|
||||||
# Get all users directly from database using UserInDB (the actual database model)
|
# Get all users via interface method (returns Pydantic User models)
|
||||||
allUsers = appInterface.db.getRecordset(UserInDB)
|
allUserModels = appInterface.getAllUsers()
|
||||||
# Convert to cleaned dictionaries first for filtering
|
# Convert to dictionaries for filtering/sorting
|
||||||
cleanedUsers = []
|
cleanedUsers = [u.model_dump() for u in allUserModels]
|
||||||
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)
|
|
||||||
|
|
||||||
# Apply server-side filtering and sorting
|
# Apply server-side filtering and sorting
|
||||||
filteredUsers = _applyFiltersAndSort(cleanedUsers, paginationParams)
|
filteredUsers = _applyFiltersAndSort(cleanedUsers, paginationParams)
|
||||||
|
|
@ -331,11 +326,7 @@ async def get_user(
|
||||||
|
|
||||||
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
|
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
|
||||||
if context.mandateId and not context.isSysAdmin:
|
if context.mandateId and not context.isSysAdmin:
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
|
||||||
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
|
|
||||||
"userId": userId,
|
|
||||||
"mandateId": str(context.mandateId)
|
|
||||||
})
|
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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)
|
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
|
||||||
if context.mandateId and not context.isSysAdmin:
|
if context.mandateId and not context.isSysAdmin:
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
|
||||||
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
|
|
||||||
"userId": userId,
|
|
||||||
"mandateId": str(context.mandateId)
|
|
||||||
})
|
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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)
|
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
|
||||||
if context.mandateId and not context.isSysAdmin:
|
if context.mandateId and not context.isSysAdmin:
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
|
||||||
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
|
|
||||||
"userId": userId,
|
|
||||||
"mandateId": str(context.mandateId)
|
|
||||||
})
|
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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)
|
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
|
||||||
if context.mandateId and not context.isSysAdmin:
|
if context.mandateId and not context.isSysAdmin:
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
|
||||||
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
|
|
||||||
"userId": userId,
|
|
||||||
"mandateId": str(context.mandateId)
|
|
||||||
})
|
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
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)
|
# MULTI-TENANT: Verify user is in the same mandate (unless SysAdmin)
|
||||||
if context.mandateId and not context.isSysAdmin:
|
if context.mandateId and not context.isSysAdmin:
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandate = appInterface.getUserMandate(userId, str(context.mandateId))
|
||||||
userMandate = appInterface.db.getRecordset(UserMandate, recordFilter={
|
|
||||||
"userId": userId,
|
|
||||||
"mandateId": str(context.mandateId)
|
|
||||||
})
|
|
||||||
if not userMandate:
|
if not userMandate:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
|
@ -803,10 +778,9 @@ async def delete_user(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete UserMandate entries for this user first
|
# Delete UserMandate entries for this user first
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandates = appInterface.getUserMandates(userId)
|
||||||
userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"userId": userId})
|
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
appInterface.db.deleteRecord(UserMandate, um["id"])
|
appInterface.deleteUserMandate(userId, str(um.mandateId))
|
||||||
|
|
||||||
success = appInterface.deleteUser(userId)
|
success = appInterface.deleteUser(userId)
|
||||||
if not success:
|
if not success:
|
||||||
|
|
|
||||||
|
|
@ -163,16 +163,14 @@ async def update_workflow(
|
||||||
# Get workflow interface with current user context
|
# Get workflow interface with current user context
|
||||||
workflowInterface = getInterface(currentUser)
|
workflowInterface = getInterface(currentUser)
|
||||||
|
|
||||||
# Get raw workflow data from database to check permissions
|
# Get workflow using interface method to check permissions
|
||||||
workflows = workflowInterface.db.getRecordset(ChatWorkflow, recordFilter={"id": workflowId})
|
workflow = workflowInterface.getWorkflow(workflowId)
|
||||||
if not workflows:
|
if not workflow:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Workflow not found"
|
detail="Workflow not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
workflow_data = workflows[0]
|
|
||||||
|
|
||||||
# Check if user has permission to update using RBAC
|
# Check if user has permission to update using RBAC
|
||||||
if not workflowInterface.checkRbacPermission(ChatWorkflow, "update", workflowId):
|
if not workflowInterface.checkRbacPermission(ChatWorkflow, "update", workflowId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -230,6 +228,49 @@ async def get_workflow_status(
|
||||||
detail=f"Error getting workflow status: {str(e)}"
|
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
|
# API Endpoint for workflow logs with selective data transfer
|
||||||
@router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
|
@router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
|
||||||
@limiter.limit("120/minute")
|
@limiter.limit("120/minute")
|
||||||
|
|
|
||||||
|
|
@ -109,96 +109,73 @@ async def export_user_data(
|
||||||
"authenticationAuthority": str(getattr(currentUser, "authenticationAuthority", ""))
|
"authenticationAuthority": str(getattr(currentUser, "authenticationAuthority", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
# Mandate memberships
|
# Mandate memberships using interface method
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandates = rootInterface.getUserMandates(str(currentUser.id))
|
||||||
userMandates = rootInterface.db.getRecordset(
|
|
||||||
UserMandate,
|
|
||||||
recordFilter={"userId": str(currentUser.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
mandates = []
|
mandates = []
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
mandateId = um.get("mandateId")
|
mandateId = um.mandateId
|
||||||
|
|
||||||
# Get mandate details
|
# Get mandate details using interface method
|
||||||
mandateRecords = rootInterface.db.getRecordset(
|
mandate = rootInterface.getMandate(mandateId)
|
||||||
Mandate,
|
mandateName = mandate.name if mandate else "Unknown"
|
||||||
recordFilter={"id": mandateId}
|
|
||||||
)
|
|
||||||
mandateName = mandateRecords[0].get("name") if mandateRecords else "Unknown"
|
|
||||||
|
|
||||||
# Get roles for this membership
|
# Get roles for this membership
|
||||||
roleIds = rootInterface.getRoleIdsForUserMandate(um.get("id"))
|
roleIds = rootInterface.getRoleIdsForUserMandate(um.id)
|
||||||
|
|
||||||
mandates.append({
|
mandates.append({
|
||||||
"userMandateId": um.get("id"),
|
"userMandateId": um.id,
|
||||||
"mandateId": mandateId,
|
"mandateId": mandateId,
|
||||||
"mandateName": mandateName,
|
"mandateName": mandateName,
|
||||||
"enabled": um.get("enabled", True),
|
"enabled": um.enabled,
|
||||||
"roleIds": roleIds,
|
"roleIds": roleIds,
|
||||||
"joinedAt": um.get("createdAt")
|
"joinedAt": um.createdAt
|
||||||
})
|
})
|
||||||
|
|
||||||
# Feature access records
|
# Feature access records using interface method
|
||||||
from modules.datamodels.datamodelMembership import FeatureAccess
|
featureAccesses = rootInterface.getFeatureAccessesForUser(str(currentUser.id))
|
||||||
featureAccesses = rootInterface.db.getRecordset(
|
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": str(currentUser.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
featureAccessList = []
|
featureAccessList = []
|
||||||
for fa in featureAccesses:
|
for fa in featureAccesses:
|
||||||
instanceId = fa.get("featureInstanceId")
|
instanceId = fa.featureInstanceId
|
||||||
|
|
||||||
# Get instance details
|
# Get instance details using interface method
|
||||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
instance = rootInterface.getFeatureInstance(instanceId)
|
||||||
instanceRecords = rootInterface.db.getRecordset(
|
|
||||||
FeatureInstance,
|
|
||||||
recordFilter={"id": instanceId}
|
|
||||||
)
|
|
||||||
|
|
||||||
instanceInfo = instanceRecords[0] if instanceRecords else {}
|
roleIds = rootInterface.getRoleIdsForFeatureAccess(fa.id)
|
||||||
roleIds = rootInterface.getRoleIdsForFeatureAccess(fa.get("id"))
|
|
||||||
|
|
||||||
featureAccessList.append({
|
featureAccessList.append({
|
||||||
"featureAccessId": fa.get("id"),
|
"featureAccessId": fa.id,
|
||||||
"featureInstanceId": instanceId,
|
"featureInstanceId": instanceId,
|
||||||
"featureCode": instanceInfo.get("featureCode"),
|
"featureCode": instance.featureCode if instance else None,
|
||||||
"instanceLabel": instanceInfo.get("label"),
|
"instanceLabel": instance.label if instance else None,
|
||||||
"enabled": fa.get("enabled", True),
|
"enabled": fa.enabled,
|
||||||
"roleIds": roleIds
|
"roleIds": roleIds
|
||||||
})
|
})
|
||||||
|
|
||||||
# Invitations created by user
|
# Invitations created by user using interface method
|
||||||
from modules.datamodels.datamodelInvitation import Invitation
|
invitationsCreated = rootInterface.getInvitationsByCreator(str(currentUser.id))
|
||||||
invitationsCreated = rootInterface.db.getRecordset(
|
|
||||||
Invitation,
|
|
||||||
recordFilter={"createdBy": str(currentUser.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
invitationsCreatedList = [
|
invitationsCreatedList = [
|
||||||
{
|
{
|
||||||
"id": inv.get("id"),
|
"id": inv.id,
|
||||||
"mandateId": inv.get("mandateId"),
|
"mandateId": inv.mandateId,
|
||||||
"createdAt": inv.get("createdAt"),
|
"createdAt": inv.createdAt,
|
||||||
"expiresAt": inv.get("expiresAt"),
|
"expiresAt": inv.expiresAt,
|
||||||
"maxUses": inv.get("maxUses"),
|
"maxUses": inv.maxUses,
|
||||||
"currentUses": inv.get("currentUses")
|
"currentUses": inv.currentUses
|
||||||
}
|
}
|
||||||
for inv in invitationsCreated
|
for inv in invitationsCreated
|
||||||
]
|
]
|
||||||
|
|
||||||
# Invitations used by user
|
# Invitations used by user using interface method
|
||||||
invitationsUsed = rootInterface.db.getRecordset(
|
invitationsUsed = rootInterface.getInvitationsByUsedBy(str(currentUser.id))
|
||||||
Invitation,
|
|
||||||
recordFilter={"usedBy": str(currentUser.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
invitationsUsedList = [
|
invitationsUsedList = [
|
||||||
{
|
{
|
||||||
"id": inv.get("id"),
|
"id": inv.id,
|
||||||
"mandateId": inv.get("mandateId"),
|
"mandateId": inv.mandateId,
|
||||||
"usedAt": inv.get("usedAt")
|
"usedAt": inv.usedAt
|
||||||
}
|
}
|
||||||
for inv in invitationsUsed
|
for inv in invitationsUsed
|
||||||
]
|
]
|
||||||
|
|
@ -262,26 +239,18 @@ async def export_portable_data(
|
||||||
"additionalProperty": []
|
"additionalProperty": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add mandate memberships as organization affiliations
|
# Add mandate memberships as organization affiliations using interface method
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
userMandates = rootInterface.getUserMandates(str(currentUser.id))
|
||||||
userMandates = rootInterface.db.getRecordset(
|
|
||||||
UserMandate,
|
|
||||||
recordFilter={"userId": str(currentUser.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
affiliations = []
|
affiliations = []
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
mandateRecords = rootInterface.db.getRecordset(
|
mandate = rootInterface.getMandate(um.mandateId)
|
||||||
Mandate,
|
if mandate:
|
||||||
recordFilter={"id": um.get("mandateId")}
|
|
||||||
)
|
|
||||||
if mandateRecords:
|
|
||||||
mandate = mandateRecords[0]
|
|
||||||
affiliations.append({
|
affiliations.append({
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"identifier": um.get("mandateId"),
|
"identifier": um.mandateId,
|
||||||
"name": mandate.get("name"),
|
"name": mandate.name,
|
||||||
"membershipActive": um.get("enabled", True)
|
"membershipActive": um.enabled
|
||||||
})
|
})
|
||||||
|
|
||||||
if affiliations:
|
if affiliations:
|
||||||
|
|
@ -370,15 +339,12 @@ async def delete_account(
|
||||||
# Step 2: Revoke invitations BEFORE generic deletion (business logic)
|
# Step 2: Revoke invitations BEFORE generic deletion (business logic)
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
from modules.datamodels.datamodelInvitation import Invitation
|
from modules.datamodels.datamodelInvitation import Invitation
|
||||||
userInvitations = rootInterface.db.getRecordset(
|
userInvitations = rootInterface.getInvitationsByCreator(str(currentUser.id))
|
||||||
Invitation,
|
|
||||||
recordFilter={"createdBy": str(currentUser.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
for inv in userInvitations:
|
for inv in userInvitations:
|
||||||
rootInterface.db.recordModify(
|
rootInterface.db.recordModify(
|
||||||
Invitation,
|
Invitation,
|
||||||
inv.get("id"),
|
inv.id,
|
||||||
{"revokedAt": getUtcTimestamp()}
|
{"revokedAt": getUtcTimestamp()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,17 +131,14 @@ async def create_invitation(
|
||||||
|
|
||||||
# Validate role IDs exist and belong to this mandate or are global
|
# Validate role IDs exist and belong to this mandate or are global
|
||||||
for roleId in data.roleIds:
|
for roleId in data.roleIds:
|
||||||
from modules.datamodels.datamodelRbac import Role
|
role = rootInterface.getRole(roleId)
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
if not role:
|
||||||
if not roleRecords:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Role '{roleId}' not found"
|
detail=f"Role '{roleId}' not found"
|
||||||
)
|
)
|
||||||
role = roleRecords[0]
|
|
||||||
# Role must be global or belong to this mandate
|
# Role must be global or belong to this mandate
|
||||||
roleMandateId = role.get("mandateId")
|
if role.mandateId and str(role.mandateId) != str(context.mandateId):
|
||||||
if roleMandateId and str(roleMandateId) != str(context.mandateId):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=f"Role '{roleId}' belongs to a different mandate"
|
detail=f"Role '{roleId}' belongs to a different mandate"
|
||||||
|
|
@ -149,18 +146,13 @@ async def create_invitation(
|
||||||
|
|
||||||
# Validate feature instance if provided
|
# Validate feature instance if provided
|
||||||
if data.featureInstanceId:
|
if data.featureInstanceId:
|
||||||
from modules.datamodels.datamodelFeatures import FeatureInstance
|
instance = rootInterface.getFeatureInstance(data.featureInstanceId)
|
||||||
instanceRecords = rootInterface.db.getRecordset(
|
if not instance:
|
||||||
FeatureInstance,
|
|
||||||
recordFilter={"id": data.featureInstanceId}
|
|
||||||
)
|
|
||||||
if not instanceRecords:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Feature instance '{data.featureInstanceId}' not found"
|
detail=f"Feature instance '{data.featureInstanceId}' not found"
|
||||||
)
|
)
|
||||||
instance = instanceRecords[0]
|
if str(instance.mandateId) != str(context.mandateId):
|
||||||
if str(instance.get("mandateId")) != str(context.mandateId):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Feature instance belongs to a different mandate"
|
detail="Feature instance belongs to a different mandate"
|
||||||
|
|
@ -196,14 +188,9 @@ async def create_invitation(
|
||||||
if data.email:
|
if data.email:
|
||||||
try:
|
try:
|
||||||
from modules.connectors.connectorMessagingEmail import ConnectorMessagingEmail
|
from modules.connectors.connectorMessagingEmail import ConnectorMessagingEmail
|
||||||
from modules.datamodels.datamodelUam import Mandate
|
|
||||||
|
|
||||||
# Get mandate name for the email
|
# Get mandate name for the email
|
||||||
mandateRecords = rootInterface.db.getRecordset(
|
mandate = rootInterface.getMandate(str(context.mandateId))
|
||||||
Mandate,
|
mandateName = mandate.name if mandate else "PowerOn"
|
||||||
recordFilter={"id": str(context.mandateId)}
|
|
||||||
)
|
|
||||||
mandateName = mandateRecords[0].get("name", "PowerOn") if mandateRecords else "PowerOn"
|
|
||||||
|
|
||||||
emailConnector = ConnectorMessagingEmail()
|
emailConnector = ConnectorMessagingEmail()
|
||||||
emailSubject = f"Einladung zu {mandateName}"
|
emailSubject = f"Einladung zu {mandateName}"
|
||||||
|
|
@ -259,14 +246,10 @@ async def create_invitation(
|
||||||
existingUser = rootInterface.getUserByUsername(data.targetUsername)
|
existingUser = rootInterface.getUserByUsername(data.targetUsername)
|
||||||
if existingUser:
|
if existingUser:
|
||||||
from modules.routes.routeNotifications import createInvitationNotification
|
from modules.routes.routeNotifications import createInvitationNotification
|
||||||
from modules.datamodels.datamodelUam import Mandate
|
|
||||||
|
|
||||||
# Get mandate name for notification
|
# Get mandate name for notification
|
||||||
mandateRecords = rootInterface.db.getRecordset(
|
mandate = rootInterface.getMandate(str(context.mandateId))
|
||||||
Mandate,
|
mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else "PowerOn"
|
||||||
recordFilter={"id": str(context.mandateId)}
|
|
||||||
)
|
|
||||||
mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
|
|
||||||
inviterName = context.user.fullName or context.user.username
|
inviterName = context.user.fullName or context.user.username
|
||||||
|
|
||||||
createInvitationNotification(
|
createInvitationNotification(
|
||||||
|
|
@ -348,38 +331,38 @@ async def list_invitations(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get all invitations for this mandate
|
# Get all invitations for this mandate (Pydantic models)
|
||||||
allInvitations = rootInterface.db.getRecordset(
|
allInvitations = rootInterface.getInvitationsByMandate(str(context.mandateId))
|
||||||
Invitation,
|
|
||||||
recordFilter={"mandateId": str(context.mandateId)}
|
|
||||||
)
|
|
||||||
|
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
for inv in allInvitations:
|
for inv in allInvitations:
|
||||||
# Skip revoked invitations
|
# Skip revoked invitations
|
||||||
if inv.get("revokedAt"):
|
if inv.revokedAt:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Filter by usage
|
# 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
|
continue
|
||||||
|
|
||||||
# Filter by expiration
|
# Filter by expiration
|
||||||
if not includeExpired and inv.get("expiresAt", 0) < currentTime:
|
expiresAt = inv.expiresAt or 0
|
||||||
|
if not includeExpired and expiresAt < currentTime:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build invite URL
|
# Build invite URL
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
frontendUrl = APP_CONFIG.get("APP_FRONTEND_URL", "http://localhost:8080")
|
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({
|
result.append({
|
||||||
**{k: v for k, v in inv.items() if not k.startswith("_")},
|
**inv.model_dump(),
|
||||||
"inviteUrl": inviteUrl,
|
"inviteUrl": inviteUrl,
|
||||||
"isExpired": inv.get("expiresAt", 0) < currentTime,
|
"isExpired": expiresAt < currentTime,
|
||||||
"isUsedUp": inv.get("currentUses", 0) >= inv.get("maxUses", 1)
|
"isUsedUp": currentUses >= maxUses
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -425,29 +408,24 @@ async def revoke_invitation(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get invitation
|
# Get invitation (Pydantic model)
|
||||||
invitationRecords = rootInterface.db.getRecordset(
|
invitation = rootInterface.getInvitation(invitationId)
|
||||||
Invitation,
|
|
||||||
recordFilter={"id": invitationId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not invitationRecords:
|
if not invitation:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Invitation '{invitationId}' not found"
|
detail=f"Invitation '{invitationId}' not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
invitation = invitationRecords[0]
|
|
||||||
|
|
||||||
# Verify mandate access
|
# Verify mandate access
|
||||||
if str(invitation.get("mandateId")) != str(context.mandateId):
|
if str(invitation.mandateId) != str(context.mandateId):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Access denied to this invitation"
|
detail="Access denied to this invitation"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Already revoked?
|
# Already revoked?
|
||||||
if invitation.get("revokedAt"):
|
if invitation.revokedAt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation is already revoked"
|
detail="Invitation is already revoked"
|
||||||
|
|
@ -496,13 +474,10 @@ async def validate_invitation(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Find invitation by token
|
# Find invitation by token (Pydantic model)
|
||||||
invitationRecords = rootInterface.db.getRecordset(
|
invitation = rootInterface.getInvitationByToken(token)
|
||||||
Invitation,
|
|
||||||
recordFilter={"token": token}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not invitationRecords:
|
if not invitation:
|
||||||
return InvitationValidation(
|
return InvitationValidation(
|
||||||
valid=False,
|
valid=False,
|
||||||
reason="Invitation not found",
|
reason="Invitation not found",
|
||||||
|
|
@ -511,10 +486,8 @@ async def validate_invitation(
|
||||||
roleIds=[]
|
roleIds=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
invitation = invitationRecords[0]
|
|
||||||
|
|
||||||
# Check if revoked
|
# Check if revoked
|
||||||
if invitation.get("revokedAt"):
|
if invitation.revokedAt:
|
||||||
return InvitationValidation(
|
return InvitationValidation(
|
||||||
valid=False,
|
valid=False,
|
||||||
reason="Invitation has been revoked",
|
reason="Invitation has been revoked",
|
||||||
|
|
@ -525,7 +498,8 @@ async def validate_invitation(
|
||||||
|
|
||||||
# Check if expired
|
# Check if expired
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
if invitation.get("expiresAt", 0) < currentTime:
|
expiresAt = invitation.expiresAt or 0
|
||||||
|
if expiresAt < currentTime:
|
||||||
return InvitationValidation(
|
return InvitationValidation(
|
||||||
valid=False,
|
valid=False,
|
||||||
reason="Invitation has expired",
|
reason="Invitation has expired",
|
||||||
|
|
@ -535,7 +509,9 @@ async def validate_invitation(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if used up
|
# 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(
|
return InvitationValidation(
|
||||||
valid=False,
|
valid=False,
|
||||||
reason="Invitation has reached maximum uses",
|
reason="Invitation has reached maximum uses",
|
||||||
|
|
@ -545,34 +521,29 @@ async def validate_invitation(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get additional info for display
|
# Get additional info for display
|
||||||
mandateId = invitation.get("mandateId")
|
mandateId = invitation.mandateId
|
||||||
mandateName = None
|
mandateName = None
|
||||||
roleLabels = []
|
roleLabels = []
|
||||||
targetUsername = invitation.get("targetUsername")
|
targetUsername = invitation.targetUsername
|
||||||
|
|
||||||
# Get mandate name
|
# Get mandate name
|
||||||
from modules.datamodels.datamodelUam import Mandate
|
mandate = rootInterface.getMandate(str(mandateId)) if mandateId else None
|
||||||
mandateRecords = rootInterface.db.getRecordset(
|
if mandate:
|
||||||
Mandate,
|
mandateName = mandate.name
|
||||||
recordFilter={"id": mandateId}
|
|
||||||
)
|
|
||||||
if mandateRecords:
|
|
||||||
mandateName = mandateRecords[0].get("name")
|
|
||||||
|
|
||||||
# Get role names
|
# Get role names
|
||||||
roleIds = invitation.get("roleIds", [])
|
roleIds = invitation.roleIds or []
|
||||||
from modules.datamodels.datamodelRbac import Role
|
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
roleLabels.append(roleRecords[0].get("roleLabel", roleId))
|
roleLabels.append(role.roleLabel)
|
||||||
|
|
||||||
return InvitationValidation(
|
return InvitationValidation(
|
||||||
valid=True,
|
valid=True,
|
||||||
reason=None,
|
reason=None,
|
||||||
mandateId=mandateId,
|
mandateId=str(mandateId) if mandateId else None,
|
||||||
mandateName=mandateName,
|
mandateName=mandateName,
|
||||||
featureInstanceId=invitation.get("featureInstanceId"),
|
featureInstanceId=str(invitation.featureInstanceId) if invitation.featureInstanceId else None,
|
||||||
roleIds=roleIds,
|
roleIds=roleIds,
|
||||||
roleLabels=roleLabels,
|
roleLabels=roleLabels,
|
||||||
targetUsername=targetUsername
|
targetUsername=targetUsername
|
||||||
|
|
@ -608,42 +579,40 @@ async def accept_invitation(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Find invitation by token
|
# Find invitation by token (Pydantic model)
|
||||||
invitationRecords = rootInterface.db.getRecordset(
|
invitation = rootInterface.getInvitationByToken(token)
|
||||||
Invitation,
|
|
||||||
recordFilter={"token": token}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not invitationRecords:
|
if not invitation:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Invitation not found"
|
detail="Invitation not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
invitation = invitationRecords[0]
|
|
||||||
|
|
||||||
# Validate invitation
|
# Validate invitation
|
||||||
if invitation.get("revokedAt"):
|
if invitation.revokedAt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has been revoked"
|
detail="Invitation has been revoked"
|
||||||
)
|
)
|
||||||
|
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
if invitation.get("expiresAt", 0) < currentTime:
|
expiresAt = invitation.expiresAt or 0
|
||||||
|
if expiresAt < currentTime:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has expired"
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has reached maximum uses"
|
detail="Invitation has reached maximum uses"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate username matches - the invitation is bound to a specific user
|
# Validate username matches - the invitation is bound to a specific user
|
||||||
targetUsername = invitation.get("targetUsername")
|
targetUsername = invitation.targetUsername
|
||||||
if targetUsername and currentUser.username != targetUsername:
|
if targetUsername and currentUser.username != targetUsername:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"User {currentUser.username} tried to accept invitation meant for {targetUsername}"
|
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"
|
detail=f"Diese Einladung ist für Benutzer '{targetUsername}' bestimmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
mandateId = invitation.get("mandateId")
|
mandateId = str(invitation.mandateId) if invitation.mandateId else None
|
||||||
roleIds = invitation.get("roleIds", [])
|
roleIds = invitation.roleIds or []
|
||||||
featureInstanceId = invitation.get("featureInstanceId")
|
featureInstanceId = str(invitation.featureInstanceId) if invitation.featureInstanceId else None
|
||||||
|
|
||||||
# Check if user is already a member
|
# Check if user is already a member
|
||||||
existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId)
|
existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId)
|
||||||
|
|
@ -744,22 +713,19 @@ def _hasMandateAdminRole(context: RequestContext) -> bool:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
from modules.datamodels.datamodelRbac import Role
|
|
||||||
|
|
||||||
for roleId in context.roleIds:
|
for roleId in context.roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
# Admin role at mandate level (not feature-instance level)
|
||||||
roleLabel = role.get("roleLabel", "")
|
if role.roleLabel == "admin" and role.mandateId and not role.featureInstanceId:
|
||||||
# Admin role at mandate level
|
|
||||||
if roleLabel == "admin" and role.get("mandateId") and not role.get("featureInstanceId"):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error checking mandate admin role: {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:
|
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.
|
Check if a role belongs to a specific feature instance.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from modules.datamodels.datamodelRbac import Role
|
role = interface.getRole(roleId)
|
||||||
roleRecords = interface.db.getRecordset(Role, recordFilter={"id": roleId})
|
if role:
|
||||||
if roleRecords:
|
return str(role.featureInstanceId or "") == str(featureInstanceId)
|
||||||
role = roleRecords[0]
|
|
||||||
return str(role.get("featureInstanceId", "")) == str(featureInstanceId)
|
|
||||||
return False
|
return False
|
||||||
except Exception:
|
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()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
for roleId in context.roleIds:
|
for roleId in context.roleIds:
|
||||||
roleRecords = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roleRecords:
|
if role:
|
||||||
role = roleRecords[0]
|
roleLabel = role.roleLabel
|
||||||
roleLabel = role.get("roleLabel", "")
|
|
||||||
# Admin role at mandate level or system admin
|
# Admin role at mandate level or system admin
|
||||||
if roleLabel in ("admin", "sysadmin"):
|
if roleLabel in ("admin", "sysadmin"):
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -137,23 +137,19 @@ async def getNotifications(
|
||||||
|
|
||||||
# Build filter
|
# Build filter
|
||||||
recordFilter = {"userId": str(currentUser.id)}
|
recordFilter = {"userId": str(currentUser.id)}
|
||||||
if status:
|
# Get notifications (Pydantic models, sorted and limited)
|
||||||
recordFilter["status"] = status
|
notifications = rootInterface.getNotificationsByUser(
|
||||||
if type:
|
userId=str(currentUser.id),
|
||||||
recordFilter["type"] = type
|
status=status,
|
||||||
|
limit=limit
|
||||||
# Get notifications
|
|
||||||
notifications = rootInterface.db.getRecordset(
|
|
||||||
model_class=UserNotification,
|
|
||||||
recordFilter=recordFilter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sort by creation date (newest first) and limit
|
# Apply type filter if needed (not common, so filter post-fetch)
|
||||||
notifications = sorted(notifications, key=lambda x: x.get("createdAt", 0), reverse=True)
|
if type:
|
||||||
if limit:
|
notifications = [n for n in notifications if n.type == type]
|
||||||
notifications = notifications[:limit]
|
|
||||||
|
|
||||||
return notifications
|
# Convert to dicts for response
|
||||||
|
return [n.model_dump() for n in notifications]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting notifications: {e}")
|
logger.error(f"Error getting notifications: {e}")
|
||||||
|
|
@ -176,12 +172,10 @@ async def getUnreadCount(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
notifications = rootInterface.db.getRecordset(
|
# Get unread notifications (Pydantic models)
|
||||||
model_class=UserNotification,
|
notifications = rootInterface.getNotificationsByUser(
|
||||||
recordFilter={
|
userId=str(currentUser.id),
|
||||||
"userId": str(currentUser.id),
|
status=NotificationStatus.UNREAD.value
|
||||||
"status": NotificationStatus.UNREAD.value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return UnreadCountResponse(count=len(notifications))
|
return UnreadCountResponse(count=len(notifications))
|
||||||
|
|
@ -207,22 +201,17 @@ async def markAsRead(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get the notification
|
# Get the notification (Pydantic model)
|
||||||
notifications = rootInterface.db.getRecordset(
|
notification = rootInterface.getNotification(notificationId)
|
||||||
model_class=UserNotification,
|
|
||||||
recordFilter={"id": notificationId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not notifications:
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification not found"
|
detail="Notification not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
notification = notifications[0]
|
|
||||||
|
|
||||||
# Verify ownership
|
# Verify ownership
|
||||||
if notification.get("userId") != currentUser.id:
|
if str(notification.userId) != str(currentUser.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to access this notification"
|
detail="Not authorized to access this notification"
|
||||||
|
|
@ -262,13 +251,10 @@ async def markAllAsRead(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get all unread notifications
|
# Get all unread notifications (Pydantic models)
|
||||||
notifications = rootInterface.db.getRecordset(
|
notifications = rootInterface.getNotificationsByUser(
|
||||||
model_class=UserNotification,
|
userId=str(currentUser.id),
|
||||||
recordFilter={
|
status=NotificationStatus.UNREAD.value
|
||||||
"userId": currentUser.id,
|
|
||||||
"status": NotificationStatus.UNREAD.value
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
|
|
@ -277,7 +263,7 @@ async def markAllAsRead(
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
rootInterface.db.recordModify(
|
rootInterface.db.recordModify(
|
||||||
model_class=UserNotification,
|
model_class=UserNotification,
|
||||||
recordId=notification.get("id"),
|
recordId=str(notification.id),
|
||||||
record={
|
record={
|
||||||
"status": NotificationStatus.READ.value,
|
"status": NotificationStatus.READ.value,
|
||||||
"readAt": currentTime
|
"readAt": currentTime
|
||||||
|
|
@ -309,37 +295,32 @@ async def executeAction(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get the notification
|
# Get the notification (Pydantic model)
|
||||||
notifications = rootInterface.db.getRecordset(
|
notification = rootInterface.getNotification(notificationId)
|
||||||
model_class=UserNotification,
|
|
||||||
recordFilter={"id": notificationId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not notifications:
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification not found"
|
detail="Notification not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
notification = notifications[0]
|
|
||||||
|
|
||||||
# Verify ownership
|
# Verify ownership
|
||||||
if notification.get("userId") != currentUser.id:
|
if str(notification.userId) != str(currentUser.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to access this notification"
|
detail="Not authorized to access this notification"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if already actioned
|
# Check if already actioned
|
||||||
if notification.get("status") == NotificationStatus.ACTIONED.value:
|
if notification.status == NotificationStatus.ACTIONED.value:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Notification has already been actioned"
|
detail="Notification has already been actioned"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate action exists
|
# Validate action exists
|
||||||
actions = notification.get("actions", [])
|
actions = notification.actions or []
|
||||||
validActionIds = [a.get("actionId") if isinstance(a, dict) else a.actionId for a in (actions or [])]
|
validActionIds = [a.get("actionId") if isinstance(a, dict) else a.actionId for a in actions]
|
||||||
|
|
||||||
if actionRequest.actionId not in validActionIds:
|
if actionRequest.actionId not in validActionIds:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -407,22 +388,17 @@ async def _handleInvitationAction(
|
||||||
detail="No invitation reference found"
|
detail="No invitation reference found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the invitation
|
# Get the invitation (Pydantic model)
|
||||||
invitations = rootInterface.db.getRecordset(
|
invitation = rootInterface.getInvitation(invitationId)
|
||||||
model_class=Invitation,
|
|
||||||
recordFilter={"id": invitationId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not invitations:
|
if not invitation:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Invitation not found"
|
detail="Invitation not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
invitation = invitations[0]
|
|
||||||
|
|
||||||
# Verify username matches
|
# Verify username matches
|
||||||
if invitation.get("targetUsername") != currentUser.username:
|
if invitation.targetUsername != currentUser.username:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="This invitation is for a different user"
|
detail="This invitation is for a different user"
|
||||||
|
|
@ -430,19 +406,22 @@ async def _handleInvitationAction(
|
||||||
|
|
||||||
# Check if invitation is still valid
|
# Check if invitation is still valid
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
if invitation.get("expiresAt", 0) < currentTime:
|
expiresAt = invitation.expiresAt or 0
|
||||||
|
if expiresAt < currentTime:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has expired"
|
detail="Invitation has expired"
|
||||||
)
|
)
|
||||||
|
|
||||||
if invitation.get("revokedAt"):
|
if invitation.revokedAt:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has been revoked"
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Invitation has reached maximum uses"
|
detail="Invitation has reached maximum uses"
|
||||||
|
|
@ -450,59 +429,34 @@ async def _handleInvitationAction(
|
||||||
|
|
||||||
if actionId == "accept":
|
if actionId == "accept":
|
||||||
# Accept the invitation - assign roles and mandate access
|
# Accept the invitation - assign roles and mandate access
|
||||||
mandateId = invitation.get("mandateId")
|
mandateId = str(invitation.mandateId) if invitation.mandateId else None
|
||||||
roleIds = invitation.get("roleIds", [])
|
roleIds = list(invitation.roleIds or [])
|
||||||
|
|
||||||
# Ensure user gets the system "user" role for access to public UI elements (e.g. playground)
|
# Ensure user gets the system "user" role for access to public UI elements (e.g. playground)
|
||||||
userRoles = rootInterface.db.getRecordset(
|
userRole = rootInterface.getRoleByLabel("user")
|
||||||
model_class=Role,
|
if userRole:
|
||||||
recordFilter={"roleLabel": "user"}
|
userRoleId = str(userRole.id)
|
||||||
)
|
|
||||||
if userRoles:
|
|
||||||
userRoleId = userRoles[0].get("id")
|
|
||||||
if userRoleId and userRoleId not in roleIds:
|
if userRoleId and userRoleId not in roleIds:
|
||||||
roleIds = roleIds + [userRoleId]
|
roleIds = roleIds + [userRoleId]
|
||||||
logger.debug(f"Added system 'user' role {userRoleId} to invitation roles")
|
logger.debug(f"Added system 'user' role {userRoleId} to invitation roles")
|
||||||
|
|
||||||
# Get mandate name for result message
|
# Get mandate name for result message
|
||||||
mandates = rootInterface.db.getRecordset(
|
mandate = rootInterface.getMandate(mandateId) if mandateId else None
|
||||||
model_class=Mandate,
|
mandateName = mandate.mandateLabel if mandate and mandate.mandateLabel else mandateId
|
||||||
recordFilter={"id": mandateId}
|
|
||||||
)
|
|
||||||
mandateName = mandates[0].get("mandateLabel", mandateId) if mandates else mandateId
|
|
||||||
|
|
||||||
# Check if user already has this mandate
|
# Check if user already has this mandate
|
||||||
existingMemberships = rootInterface.db.getRecordset(
|
existingMembership = rootInterface.getUserMandate(str(currentUser.id), mandateId) if mandateId else None
|
||||||
model_class=UserMandate,
|
|
||||||
recordFilter={
|
|
||||||
"userId": currentUser.id,
|
|
||||||
"mandateId": mandateId
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if existingMemberships:
|
if existingMembership:
|
||||||
# Update existing membership with new roles
|
# Update existing membership with new roles via interface
|
||||||
existingMembership = existingMemberships[0]
|
# Note: roleIds on UserMandate is deprecated - roles should be assigned via UserMandateRole
|
||||||
existingRoles = existingMembership.get("roleIds", [])
|
logger.info(f"User {currentUser.id} already has membership in mandate {mandateId}, adding roles via UserMandateRole")
|
||||||
mergedRoles = list(set(existingRoles + roleIds))
|
# Add roles via junction table
|
||||||
|
for roleId in roleIds:
|
||||||
rootInterface.db.recordModify(
|
rootInterface.addRoleToUserMandate(str(existingMembership.id), roleId)
|
||||||
model_class=UserMandate,
|
|
||||||
recordId=existingMembership.get("id"),
|
|
||||||
record={"roleIds": mergedRoles}
|
|
||||||
)
|
|
||||||
logger.info(f"Updated UserMandate for user {currentUser.id} in mandate {mandateId}")
|
|
||||||
else:
|
else:
|
||||||
# Create new user-mandate relationship
|
# Create new user-mandate relationship via interface
|
||||||
userMandate = UserMandate(
|
rootInterface.createUserMandate(str(currentUser.id), mandateId, roleIds)
|
||||||
userId=currentUser.id,
|
|
||||||
mandateId=mandateId,
|
|
||||||
roleIds=roleIds
|
|
||||||
)
|
|
||||||
rootInterface.db.recordCreate(
|
|
||||||
model_class=UserMandate,
|
|
||||||
record=userMandate.model_dump()
|
|
||||||
)
|
|
||||||
logger.info(f"Created UserMandate for user {currentUser.id} in mandate {mandateId}")
|
logger.info(f"Created UserMandate for user {currentUser.id} in mandate {mandateId}")
|
||||||
|
|
||||||
# Mark invitation as used
|
# Mark invitation as used
|
||||||
|
|
@ -510,9 +464,9 @@ async def _handleInvitationAction(
|
||||||
model_class=Invitation,
|
model_class=Invitation,
|
||||||
recordId=invitationId,
|
recordId=invitationId,
|
||||||
record={
|
record={
|
||||||
"usedBy": currentUser.id,
|
"usedBy": str(currentUser.id),
|
||||||
"usedAt": currentTime,
|
"usedAt": currentTime,
|
||||||
"currentUses": invitation.get("currentUses", 0) + 1
|
"currentUses": currentUses + 1
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -545,22 +499,17 @@ async def deleteNotification(
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get the notification
|
# Get the notification (Pydantic model)
|
||||||
notifications = rootInterface.db.getRecordset(
|
notification = rootInterface.getNotification(notificationId)
|
||||||
model_class=UserNotification,
|
|
||||||
recordFilter={"id": notificationId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not notifications:
|
if not notification:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="Notification not found"
|
detail="Notification not found"
|
||||||
)
|
)
|
||||||
|
|
||||||
notification = notifications[0]
|
|
||||||
|
|
||||||
# Verify ownership
|
# Verify ownership
|
||||||
if notification.get("userId") != currentUser.id:
|
if str(notification.userId) != str(currentUser.id):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not authorized to delete this notification"
|
detail="Not authorized to delete this notification"
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,8 @@ async def list_tokens(
|
||||||
if statusFilter:
|
if statusFilter:
|
||||||
recordFilter["status"] = statusFilter
|
recordFilter["status"] = statusFilter
|
||||||
# MULTI-TENANT: SysAdmin sees ALL tokens (no mandate filter)
|
# MULTI-TENANT: SysAdmin sees ALL tokens (no mandate filter)
|
||||||
|
# Use interface method to get tokens with flexible filtering
|
||||||
tokens = appInterface.db.getRecordset(Token, recordFilter=recordFilter)
|
tokens = appInterface.getAllTokens(recordFilter=recordFilter)
|
||||||
return tokens
|
return tokens
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|
@ -254,15 +254,13 @@ async def revoke_tokens_by_mandate(
|
||||||
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
|
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
|
||||||
appInterface = getRootInterface()
|
appInterface = getRootInterface()
|
||||||
|
|
||||||
# Get all UserMandate entries for this mandate to find users
|
# Get all UserMandate entries for this mandate to find users using interface method
|
||||||
# Note: In new model, users are linked via UserMandate, not User.mandateId
|
userMandates = appInterface.getUserMandatesByMandate(mandateId)
|
||||||
from modules.datamodels.datamodelMembership import UserMandate
|
|
||||||
userMandates = appInterface.db.getRecordset(UserMandate, recordFilter={"mandateId": mandateId})
|
|
||||||
|
|
||||||
total = 0
|
total = 0
|
||||||
for um in userMandates:
|
for um in userMandates:
|
||||||
total += appInterface.revokeTokensByUser(
|
total += appInterface.revokeTokensByUser(
|
||||||
userId=um["userId"],
|
userId=um.userId,
|
||||||
authority=AuthAuthority(authority) if authority else None,
|
authority=AuthAuthority(authority) if authority else None,
|
||||||
mandateId=None, # Revoke all tokens for user
|
mandateId=None, # Revoke all tokens for user
|
||||||
revokedBy=currentUser.id,
|
revokedBy=currentUser.id,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import httpx
|
||||||
from modules.shared.configuration import APP_CONFIG
|
from modules.shared.configuration import APP_CONFIG
|
||||||
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
|
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
|
||||||
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
|
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 import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
|
||||||
from modules.auth.tokenManager import TokenManager
|
from modules.auth.tokenManager import TokenManager
|
||||||
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
||||||
|
|
@ -171,10 +171,9 @@ async def login(
|
||||||
try:
|
try:
|
||||||
if connectionId:
|
if connectionId:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
|
connection = rootInterface.getUserConnectionById(connectionId)
|
||||||
if records:
|
if connection:
|
||||||
record = records[0]
|
login_hint = connection.externalEmail or connection.externalUsername
|
||||||
login_hint = record.get("externalEmail") or record.get("externalUsername")
|
|
||||||
if login_hint:
|
if login_hint:
|
||||||
extra_params["login_hint"] = login_hint
|
extra_params["login_hint"] = login_hint
|
||||||
if "@" in login_hint:
|
if "@" in login_hint:
|
||||||
|
|
@ -260,23 +259,20 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
# Prefer connection flow reuse; fallback to user access token
|
# Prefer connection flow reuse; fallback to user access token
|
||||||
if connection_id:
|
if connection_id:
|
||||||
existing_tokens = rootInterface.db.getRecordset(Token, recordFilter={
|
existing_tokens = rootInterface.getTokensByConnectionIdAndAuthority(
|
||||||
"connectionId": connection_id,
|
connection_id, AuthAuthority.GOOGLE
|
||||||
"authority": AuthAuthority.GOOGLE
|
)
|
||||||
})
|
|
||||||
if existing_tokens:
|
if existing_tokens:
|
||||||
# Use most recent by createdAt
|
# Use most recent by createdAt
|
||||||
existing_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True)
|
existing_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
|
||||||
token_response["refresh_token"] = existing_tokens[0].get("tokenRefresh", "")
|
token_response["refresh_token"] = existing_tokens[0].tokenRefresh or ""
|
||||||
if not token_response.get("refresh_token") and user_id:
|
if not token_response.get("refresh_token") and user_id:
|
||||||
existing_access_tokens = rootInterface.db.getRecordset(Token, recordFilter={
|
existing_access_tokens = rootInterface.getTokensByUserIdNoConnection(
|
||||||
"userId": user_id,
|
user_id, AuthAuthority.GOOGLE
|
||||||
"connectionId": None,
|
)
|
||||||
"authority": AuthAuthority.GOOGLE
|
|
||||||
})
|
|
||||||
if existing_access_tokens:
|
if existing_access_tokens:
|
||||||
existing_access_tokens.sort(key=lambda x: parseTimestamp(x.get("createdAt"), default=0), reverse=True)
|
existing_access_tokens.sort(key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True)
|
||||||
token_response["refresh_token"] = existing_access_tokens[0].get("tokenRefresh", "")
|
token_response["refresh_token"] = existing_access_tokens[0].tokenRefresh or ""
|
||||||
except Exception:
|
except Exception:
|
||||||
# Non-fatal; continue without refresh token
|
# Non-fatal; continue without refresh token
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -330,40 +330,34 @@ Falls Sie sich nicht registriert haben, können Sie diese E-Mail ignorieren."""
|
||||||
from modules.datamodels.datamodelUam import Mandate
|
from modules.datamodels.datamodelUam import Mandate
|
||||||
|
|
||||||
currentTime = getUtcTimestamp()
|
currentTime = getUtcTimestamp()
|
||||||
pendingInvitations = appInterface.db.getRecordset(
|
pendingInvitations = appInterface.getInvitationsByTargetUsername(userData.username)
|
||||||
model_class=Invitation,
|
|
||||||
recordFilter={"targetUsername": userData.username}
|
|
||||||
)
|
|
||||||
|
|
||||||
for invitation in pendingInvitations:
|
for invitation in pendingInvitations:
|
||||||
# Skip expired, revoked, or fully used invitations
|
# Skip expired, revoked, or fully used invitations
|
||||||
if invitation.get("expiresAt", 0) < currentTime:
|
if (invitation.expiresAt or 0) < currentTime:
|
||||||
continue
|
continue
|
||||||
if invitation.get("revokedAt"):
|
if invitation.revokedAt:
|
||||||
continue
|
continue
|
||||||
if invitation.get("currentUses", 0) >= invitation.get("maxUses", 1):
|
if (invitation.currentUses or 0) >= (invitation.maxUses or 1):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get mandate name for notification
|
# Get mandate name for notification using interface method
|
||||||
mandateId = invitation.get("mandateId")
|
mandateId = invitation.mandateId
|
||||||
mandateRecords = appInterface.db.getRecordset(
|
mandate = appInterface.getMandate(mandateId)
|
||||||
Mandate,
|
mandateName = mandate.mandateLabel if mandate else "PowerOn"
|
||||||
recordFilter={"id": mandateId}
|
|
||||||
)
|
|
||||||
mandateName = mandateRecords[0].get("mandateLabel", "PowerOn") if mandateRecords else "PowerOn"
|
|
||||||
|
|
||||||
# Get inviter name
|
# Get inviter name
|
||||||
inviterId = invitation.get("createdBy")
|
inviterId = invitation.createdBy
|
||||||
inviter = appInterface.getUserById(inviterId) if inviterId else None
|
inviter = appInterface.getUserById(inviterId) if inviterId else None
|
||||||
inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn"
|
inviterName = (inviter.fullName or inviter.username) if inviter else "PowerOn"
|
||||||
|
|
||||||
createInvitationNotification(
|
createInvitationNotification(
|
||||||
userId=str(user.id),
|
userId=str(user.id),
|
||||||
invitationId=str(invitation.get("id")),
|
invitationId=str(invitation.id),
|
||||||
mandateName=mandateName,
|
mandateName=mandateName,
|
||||||
inviterName=inviterName
|
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:
|
except Exception as notifErr:
|
||||||
logger.warning(f"Failed to create notifications for pending invitations: {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.interfaces.interfaceDbApp import getInterface, getRootInterface
|
||||||
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
|
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
|
||||||
from modules.datamodels.datamodelSecurity import Token
|
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 import createAccessToken, setAccessTokenCookie, createRefreshToken, setRefreshTokenCookie
|
||||||
from modules.auth.tokenManager import TokenManager
|
from modules.auth.tokenManager import TokenManager
|
||||||
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
|
||||||
|
|
@ -97,11 +97,10 @@ async def login(
|
||||||
if connectionId:
|
if connectionId:
|
||||||
try:
|
try:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
# Fetch the connection by ID directly
|
# Fetch the connection by ID directly using interface method
|
||||||
records = rootInterface.db.getRecordset(UserConnection, recordFilter={"id": connectionId})
|
connection = rootInterface.getUserConnectionById(connectionId)
|
||||||
if records:
|
if connection:
|
||||||
record = records[0]
|
login_hint = connection.externalEmail or connection.externalUsername
|
||||||
login_hint = record.get("externalEmail") or record.get("externalUsername")
|
|
||||||
if login_hint:
|
if login_hint:
|
||||||
login_kwargs["login_hint"] = login_hint
|
login_kwargs["login_hint"] = login_hint
|
||||||
# Derive domain hint from email/UPN
|
# Derive domain hint from email/UPN
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ def _getUserRoleIds(userId: str) -> List[str]:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
roleIds = []
|
roleIds = []
|
||||||
|
|
||||||
userMandates = rootInterface.db.getRecordset(
|
# Get UserMandates as Pydantic models
|
||||||
UserMandate,
|
userMandates = rootInterface.getUserMandates(userId)
|
||||||
recordFilter={"userId": userId, "enabled": True}
|
|
||||||
)
|
|
||||||
|
|
||||||
for um in userMandates:
|
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:
|
for rid in mandateRoleIds:
|
||||||
if rid not in roleIds:
|
if rid not in roleIds:
|
||||||
roleIds.append(rid)
|
roleIds.append(rid)
|
||||||
|
|
@ -60,30 +60,24 @@ def _checkUiPermission(roleIds: List[str], objectKey: str) -> bool:
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
# Get UI rules for this role
|
# Get UI rules for this role (returns Pydantic AccessRule models)
|
||||||
rules = rootInterface.db.getRecordset(
|
rules = rootInterface.getAccessRules(roleId=roleId, context=AccessRuleContext.UI)
|
||||||
AccessRule,
|
|
||||||
recordFilter={"roleId": roleId, "context": "UI"}
|
|
||||||
)
|
|
||||||
|
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
ruleItem = rule.get("item")
|
if not rule.view:
|
||||||
ruleView = rule.get("view", False)
|
|
||||||
|
|
||||||
if not ruleView:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Global rule (item=None) grants access to all UI
|
# Global rule (item=None) grants access to all UI
|
||||||
if ruleItem is None:
|
if rule.item is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Exact match
|
# Exact match
|
||||||
if ruleItem == objectKey:
|
if rule.item == objectKey:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Wildcard match (e.g., ui.system.* matches ui.system.playground)
|
# Wildcard match (e.g., ui.system.* matches ui.system.playground)
|
||||||
if ruleItem.endswith(".*"):
|
if rule.item.endswith(".*"):
|
||||||
prefix = ruleItem[:-2]
|
prefix = rule.item[:-2]
|
||||||
if objectKey.startswith(prefix):
|
if objectKey.startswith(prefix):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -108,6 +102,12 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
|
||||||
elif featureCode == "realestate":
|
elif featureCode == "realestate":
|
||||||
from modules.features.realestate.mainRealEstate import UI_OBJECTS
|
from modules.features.realestate.mainRealEstate import UI_OBJECTS
|
||||||
return 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:
|
else:
|
||||||
logger.warning(f"Unknown feature code: {featureCode}")
|
logger.warning(f"Unknown feature code: {featureCode}")
|
||||||
return []
|
return []
|
||||||
|
|
@ -287,67 +287,50 @@ def _getInstanceViewPermissions(
|
||||||
permissions = {"_all": False, "isAdmin": False}
|
permissions = {"_all": False, "isAdmin": False}
|
||||||
|
|
||||||
try:
|
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
|
if not featureAccess:
|
||||||
featureAccesses = rootInterface.db.getRecordset(
|
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": userId, "featureInstanceId": instanceId}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not featureAccesses:
|
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
# Get role IDs via FeatureAccessRole junction table
|
# Get role IDs via interface method
|
||||||
featureAccessId = featureAccesses[0].get("id")
|
roleIds = rootInterface.getRoleIdsForFeatureAccess(str(featureAccess.id))
|
||||||
featureAccessRoles = rootInterface.db.getRecordset(
|
|
||||||
FeatureAccessRole,
|
|
||||||
recordFilter={"featureAccessId": featureAccessId}
|
|
||||||
)
|
|
||||||
roleIds = [far.get("roleId") for far in featureAccessRoles]
|
|
||||||
|
|
||||||
if not roleIds:
|
if not roleIds:
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
# Check if user has admin role
|
# Check if user has admin role
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
roles = rootInterface.db.getRecordset(Role, recordFilter={"id": roleId})
|
role = rootInterface.getRole(roleId)
|
||||||
if roles:
|
if role and "admin" in role.roleLabel.lower():
|
||||||
roleLabel = roles[0].get("roleLabel", "").lower()
|
permissions["isAdmin"] = True
|
||||||
if "admin" in roleLabel:
|
break
|
||||||
permissions["isAdmin"] = True
|
|
||||||
break
|
|
||||||
|
|
||||||
# Get UI permissions from AccessRules
|
# Get UI permissions from AccessRules (Pydantic models)
|
||||||
# Permissions are stored with full objectKey (e.g., ui.feature.trustee.dashboard)
|
|
||||||
for roleId in roleIds:
|
for roleId in roleIds:
|
||||||
accessRules = rootInterface.db.getRecordset(
|
accessRules = rootInterface.getAccessRules(roleId=roleId, context=AccessRuleContext.UI)
|
||||||
AccessRule,
|
|
||||||
recordFilter={"roleId": roleId, "context": "UI"}
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(f"_getInstanceViewPermissions: roleId={roleId}, UI rules count={len(accessRules)}")
|
logger.debug(f"_getInstanceViewPermissions: roleId={roleId}, UI rules count={len(accessRules)}")
|
||||||
|
|
||||||
for rule in accessRules:
|
for rule in accessRules:
|
||||||
if not rule.get("view", False):
|
if not rule.view:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item = rule.get("item")
|
logger.debug(f"_getInstanceViewPermissions: rule item={rule.item}, view={rule.view}")
|
||||||
logger.debug(f"_getInstanceViewPermissions: rule item={item}, view={rule.get('view')}")
|
|
||||||
|
|
||||||
if item is None:
|
if rule.item is None:
|
||||||
# item=None means all views
|
# item=None means all views
|
||||||
permissions["_all"] = True
|
permissions["_all"] = True
|
||||||
else:
|
else:
|
||||||
# Store full objectKey as per Navigation-API-Konzept
|
# Store full objectKey as per Navigation-API-Konzept
|
||||||
permissions[item] = True
|
permissions[rule.item] = True
|
||||||
|
|
||||||
logger.debug(f"_getInstanceViewPermissions: final permissions={permissions}")
|
logger.debug(f"_getInstanceViewPermissions: final permissions={permissions}")
|
||||||
return permissions
|
return permissions
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error getting instance view permissions: {e}")
|
logger.debug(f"Error getting instance view permissions: {e}")
|
||||||
return permissions
|
return permissions # Fail-safe: no permissions on error
|
||||||
|
|
||||||
|
|
||||||
def _buildStaticBlocks(
|
def _buildStaticBlocks(
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ class RbacClass:
|
||||||
try:
|
try:
|
||||||
# Get Root mandate ID (first mandate in system)
|
# Get Root mandate ID (first mandate in system)
|
||||||
allMandates = self.dbApp.getRecordset(Mandate)
|
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:
|
# Collect mandates to check:
|
||||||
# - If mandateId provided: current mandate + Root mandate (if different)
|
# - If mandateId provided: current mandate + Root mandate (if different)
|
||||||
|
|
@ -186,21 +186,21 @@ class RbacClass:
|
||||||
|
|
||||||
# Load roles from each mandate
|
# Load roles from each mandate
|
||||||
for checkMandateId in mandatesToCheck:
|
for checkMandateId in mandatesToCheck:
|
||||||
userMandates = self.dbApp.getRecordset(
|
userMandateRecords = self.dbApp.getRecordset(
|
||||||
UserMandate,
|
UserMandate,
|
||||||
recordFilter={"userId": user.id, "mandateId": checkMandateId, "enabled": True}
|
recordFilter={"userId": user.id, "mandateId": checkMandateId, "enabled": True}
|
||||||
)
|
)
|
||||||
|
|
||||||
if userMandates:
|
if userMandateRecords:
|
||||||
userMandateId = userMandates[0].get("id")
|
userMandateId = userMandateRecords[0]["id"]
|
||||||
|
|
||||||
# Lade UserMandateRoles (Mandate-level roles)
|
# Lade UserMandateRoles (Mandate-level roles)
|
||||||
userMandateRoles = self.dbApp.getRecordset(
|
userMandateRoleRecords = self.dbApp.getRecordset(
|
||||||
UserMandateRole,
|
UserMandateRole,
|
||||||
recordFilter={"userMandateId": userMandateId}
|
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)
|
roleIds.update(foundRoles)
|
||||||
|
|
||||||
# Load FeatureAccess + FeatureAccessRole (Instance-level roles)
|
# Load FeatureAccess + FeatureAccessRole (Instance-level roles)
|
||||||
|
|
@ -215,14 +215,14 @@ class RbacClass:
|
||||||
)
|
)
|
||||||
|
|
||||||
if featureAccessRecords:
|
if featureAccessRecords:
|
||||||
featureAccessId = featureAccessRecords[0].get("id")
|
featureAccessId = featureAccessRecords[0]["id"]
|
||||||
|
|
||||||
featureAccessRoles = self.dbApp.getRecordset(
|
featureAccessRoleRecords = self.dbApp.getRecordset(
|
||||||
FeatureAccessRole,
|
FeatureAccessRole,
|
||||||
recordFilter={"featureAccessId": featureAccessId}
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error loading role IDs for user {user.id}: {e}")
|
logger.error(f"Error loading role IDs for user {user.id}: {e}")
|
||||||
|
|
@ -377,12 +377,14 @@ class RbacClass:
|
||||||
if not roleRecords:
|
if not roleRecords:
|
||||||
continue
|
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
|
# Bestimme Priorität basierend auf Role-Scope
|
||||||
if role.get("featureInstanceId"):
|
if role.featureInstanceId:
|
||||||
priority = 3 # Instance-specific
|
priority = 3 # Instance-specific
|
||||||
elif role.get("mandateId"):
|
elif role.mandateId:
|
||||||
priority = 2 # Mandate-specific
|
priority = 2 # Mandate-specific
|
||||||
else:
|
else:
|
||||||
priority = 1 # Global
|
priority = 1 # Global
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ class ChatService:
|
||||||
"workflowId": workflow.id,
|
"workflowId": workflow.id,
|
||||||
"process": process,
|
"process": process,
|
||||||
"engine": aiResponse.modelName,
|
"engine": aiResponse.modelName,
|
||||||
"priceUsd": aiResponse.priceUsd,
|
"priceCHF": aiResponse.priceCHF,
|
||||||
"processingTime": aiResponse.processingTime,
|
"processingTime": aiResponse.processingTime,
|
||||||
"bytesSent": aiResponse.bytesSent,
|
"bytesSent": aiResponse.bytesSent,
|
||||||
"bytesReceived": aiResponse.bytesReceived,
|
"bytesReceived": aiResponse.bytesReceived,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class ExtractionService:
|
||||||
# Verify required internal model is available (used for pricing in extractContent)
|
# Verify required internal model is available (used for pricing in extractContent)
|
||||||
modelDisplayName = "Internal Document Extractor"
|
modelDisplayName = "Internal Document Extractor"
|
||||||
model = modelRegistry.getModel(modelDisplayName)
|
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.")
|
raise RuntimeError(f"FATAL: Required internal model '{modelDisplayName}' is not available. Check connector registration.")
|
||||||
|
|
||||||
def extractContent(
|
def extractContent(
|
||||||
|
|
@ -218,18 +218,18 @@ class ExtractionService:
|
||||||
modelDisplayName = "Internal Document Extractor"
|
modelDisplayName = "Internal Document Extractor"
|
||||||
model = modelRegistry.getModel(modelDisplayName)
|
model = modelRegistry.getModel(modelDisplayName)
|
||||||
# Hard fail if model is missing; caller must ensure connectors are registered
|
# 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:
|
if docOperationId:
|
||||||
self.services.chat.progressLogFinish(docOperationId, False)
|
self.services.chat.progressLogFinish(docOperationId, False)
|
||||||
raise RuntimeError(f"Pricing model not available: {modelDisplayName}")
|
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
|
# Create AiCallResponse with real calculation
|
||||||
# Use model.name for the response (API identifier), not displayName
|
# Use model.name for the response (API identifier), not displayName
|
||||||
aiResponse = AiCallResponse(
|
aiResponse = AiCallResponse(
|
||||||
content="", # No content for extraction stats needed
|
content="", # No content for extraction stats needed
|
||||||
modelName=model.name,
|
modelName=model.name,
|
||||||
priceUsd=priceUsd,
|
priceCHF=priceCHF,
|
||||||
processingTime=processingTime,
|
processingTime=processingTime,
|
||||||
bytesSent=bytesSent,
|
bytesSent=bytesSent,
|
||||||
bytesReceived=bytesReceived,
|
bytesReceived=bytesReceived,
|
||||||
|
|
@ -478,7 +478,7 @@ class ExtractionService:
|
||||||
"resultSize": len(response.content),
|
"resultSize": len(response.content),
|
||||||
"typeGroup": part.typeGroup,
|
"typeGroup": part.typeGroup,
|
||||||
"modelName": response.modelName,
|
"modelName": response.modelName,
|
||||||
"priceUsd": response.priceUsd
|
"priceCHF": response.priceCHF
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -606,7 +606,7 @@ class ExtractionService:
|
||||||
"originalIndex": i, # Phase 7: Explicit order index
|
"originalIndex": i, # Phase 7: Explicit order index
|
||||||
"processingOrder": i, # Phase 7: Processing order
|
"processingOrder": i, # Phase 7: Processing order
|
||||||
"modelName": result.modelName,
|
"modelName": result.modelName,
|
||||||
"priceUsd": result.priceUsd,
|
"priceCHF": result.priceCHF,
|
||||||
"processingTime": result.processingTime,
|
"processingTime": result.processingTime,
|
||||||
"bytesSent": result.bytesSent,
|
"bytesSent": result.bytesSent,
|
||||||
"bytesReceived": result.bytesReceived
|
"bytesReceived": result.bytesReceived
|
||||||
|
|
@ -1311,7 +1311,7 @@ class ExtractionService:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=modelResponse.content,
|
content=modelResponse.content,
|
||||||
modelName=model.name,
|
modelName=model.name,
|
||||||
priceUsd=0.0,
|
priceCHF=0.0,
|
||||||
processingTime=processingTime,
|
processingTime=processingTime,
|
||||||
bytesSent=0,
|
bytesSent=0,
|
||||||
bytesReceived=0,
|
bytesReceived=0,
|
||||||
|
|
@ -1416,7 +1416,7 @@ class ExtractionService:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=mergedContent,
|
content=mergedContent,
|
||||||
modelName=model.name,
|
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),
|
processingTime=sum(r.processingTime for r in chunkResults),
|
||||||
bytesSent=sum(r.bytesSent for r in chunkResults),
|
bytesSent=sum(r.bytesSent for r in chunkResults),
|
||||||
bytesReceived=sum(r.bytesReceived for r in chunkResults),
|
bytesReceived=sum(r.bytesReceived for r in chunkResults),
|
||||||
|
|
@ -1465,7 +1465,7 @@ class ExtractionService:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=mergedContent,
|
content=mergedContent,
|
||||||
modelName=model.name,
|
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),
|
processingTime=sum(r.processingTime for r in chunkResults),
|
||||||
bytesSent=sum(r.bytesSent for r in chunkResults),
|
bytesSent=sum(r.bytesSent for r in chunkResults),
|
||||||
bytesReceived=sum(r.bytesReceived for r in chunkResults),
|
bytesReceived=sum(r.bytesReceived for r in chunkResults),
|
||||||
|
|
@ -1492,7 +1492,7 @@ class ExtractionService:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=errorMsg,
|
content=errorMsg,
|
||||||
modelName="error",
|
modelName="error",
|
||||||
priceUsd=0.0,
|
priceCHF=0.0,
|
||||||
processingTime=0.0,
|
processingTime=0.0,
|
||||||
bytesSent=inputBytes,
|
bytesSent=inputBytes,
|
||||||
bytesReceived=outputBytes,
|
bytesReceived=outputBytes,
|
||||||
|
|
@ -1622,7 +1622,7 @@ class ExtractionService:
|
||||||
return AiCallResponse(
|
return AiCallResponse(
|
||||||
content=mergedContent,
|
content=mergedContent,
|
||||||
modelName="multiple",
|
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),
|
processingTime=sum(r.processingTime for r in allResults),
|
||||||
bytesSent=sum(r.bytesSent for r in allResults),
|
bytesSent=sum(r.bytesSent for r in allResults),
|
||||||
bytesReceived=sum(r.bytesReceived 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()
|
rootInterface = getRootInterface()
|
||||||
|
|
||||||
# Get all feature accesses for this user
|
# Get all feature accesses for this user using interface method
|
||||||
featureAccesses = rootInterface.db.getRecordset(
|
featureAccesses = rootInterface.getFeatureAccessesForUser(str(userId))
|
||||||
FeatureAccess,
|
|
||||||
recordFilter={"userId": str(userId)}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Collect unique feature codes
|
# Collect unique feature codes
|
||||||
featureCodes: Set[str] = set()
|
featureCodes: Set[str] = set()
|
||||||
for fa in featureAccesses:
|
for fa in featureAccesses:
|
||||||
instanceId = fa.get("featureInstanceId")
|
instanceId = fa.featureInstanceId
|
||||||
instanceRecords = rootInterface.db.getRecordset(
|
instance = rootInterface.getFeatureInstance(instanceId)
|
||||||
FeatureInstance,
|
if instance:
|
||||||
recordFilter={"id": instanceId}
|
featureCode = instance.featureCode
|
||||||
)
|
|
||||||
if instanceRecords:
|
|
||||||
featureCode = instanceRecords[0].get("featureCode")
|
|
||||||
if featureCode:
|
if featureCode:
|
||||||
featureCodes.add(featureCode)
|
featureCodes.add(featureCode)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ FEATURE_ICON = "mdi-cog"
|
||||||
# Block Order (gemäss Navigation-API-Konzept):
|
# Block Order (gemäss Navigation-API-Konzept):
|
||||||
# - System: 10
|
# - System: 10
|
||||||
# - <dynamic/features>: 15 (wird in routeSystem.py eingefügt)
|
# - <dynamic/features>: 15 (wird in routeSystem.py eingefügt)
|
||||||
# - Workflows: 20
|
|
||||||
# - Basisdaten: 30
|
# - Basisdaten: 30
|
||||||
# - Migrate: 40
|
|
||||||
# - Administration: 200
|
# - Administration: 200
|
||||||
#
|
#
|
||||||
|
# NOTE: Workflows and Migrate sections removed - now handled as features
|
||||||
|
#
|
||||||
# Item Order: Default-Abstand 10 pro Item
|
# Item Order: Default-Abstand 10 pro Item
|
||||||
# uiComponent: Abgeleitet von objectKey (ui.system.home -> page.system.home)
|
# uiComponent: Abgeleitet von objectKey (ui.system.home -> page.system.home)
|
||||||
# icon: Wird intern gehalten aber NICHT in der API Response zurückgegeben
|
# 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",
|
"id": "basedata",
|
||||||
"title": {"en": "BASE DATA", "de": "BASISDATEN", "fr": "DONNÉES DE BASE"},
|
"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",
|
"id": "admin",
|
||||||
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
|
"title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
|
||||||
"order": 200,
|
"order": 200,
|
||||||
"adminOnly": True,
|
"adminOnly": True,
|
||||||
"items": [
|
"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",
|
"id": "admin-mandates",
|
||||||
"objectKey": "ui.admin.mandates",
|
"objectKey": "ui.admin.mandates",
|
||||||
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
|
"label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
|
||||||
"icon": "FaBuilding",
|
"icon": "FaBuilding",
|
||||||
"path": "/admin/mandates",
|
"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,
|
"adminOnly": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -190,27 +148,36 @@ NAVIGATION_SECTIONS = [
|
||||||
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
|
"label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
|
||||||
"icon": "FaBuilding",
|
"icon": "FaBuilding",
|
||||||
"path": "/admin/access",
|
"path": "/admin/access",
|
||||||
"order": 5,
|
"order": 30,
|
||||||
"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,
|
|
||||||
"adminOnly": True,
|
"adminOnly": True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "admin-roles",
|
"id": "admin-roles",
|
||||||
"objectKey": "ui.admin.roles",
|
"objectKey": "ui.admin.roles",
|
||||||
"label": {"en": "Roles & Permissions", "de": "Rollen & Berechtigungen", "fr": "Rôles et permissions"},
|
"label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
|
||||||
"icon": "FaKey",
|
"icon": "FaUserTag",
|
||||||
"path": "/admin/mandate-roles",
|
"path": "/admin/mandate-roles",
|
||||||
"order": 40,
|
"order": 40,
|
||||||
"adminOnly": True,
|
"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(
|
metadata=AiResponseMetadata(
|
||||||
additionalData={
|
additionalData={
|
||||||
"modelName": aiResponse_obj.modelName,
|
"modelName": aiResponse_obj.modelName,
|
||||||
"priceUsd": aiResponse_obj.priceUsd,
|
"priceCHF": aiResponse_obj.priceCHF,
|
||||||
"processingTime": aiResponse_obj.processingTime,
|
"processingTime": aiResponse_obj.processingTime,
|
||||||
"bytesSent": aiResponse_obj.bytesSent,
|
"bytesSent": aiResponse_obj.bytesSent,
|
||||||
"bytesReceived": aiResponse_obj.bytesReceived,
|
"bytesReceived": aiResponse_obj.bytesReceived,
|
||||||
|
|
|
||||||
|
|
@ -628,7 +628,7 @@ Width: {crawlWidth}
|
||||||
"hasContent": True,
|
"hasContent": True,
|
||||||
"error": None,
|
"error": None,
|
||||||
"modelUsed": modelName,
|
"modelUsed": modelName,
|
||||||
"priceUsd": 0.0,
|
"priceCHF": 0.0,
|
||||||
"bytesSent": 0,
|
"bytesSent": 0,
|
||||||
"bytesReceived": contentLength,
|
"bytesReceived": contentLength,
|
||||||
"isValidJson": True,
|
"isValidJson": True,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue