fiixed feature instance role access

This commit is contained in:
patrick-motsch 2026-02-08 14:26:01 +01:00
parent f15ed2e380
commit 8d28f6d77b
33 changed files with 764 additions and 376 deletions

View file

@ -88,7 +88,9 @@ class AutomationObjects:
permissions = self.rbac.getUserPermissions( permissions = self.rbac.getUserPermissions(
user=self.currentUser, user=self.currentUser,
context=AccessRuleContext.DATA, context=AccessRuleContext.DATA,
item=objectKey item=objectKey,
mandateId=self.mandateId,
featureInstanceId=self.featureInstanceId
) )
accessLevel = getattr(permissions, action, AccessLevel.NONE) accessLevel = getattr(permissions, action, AccessLevel.NONE)
@ -373,6 +375,21 @@ class AutomationObjects:
logger.error(f"Error creating automation definition: {str(e)}") logger.error(f"Error creating automation definition: {str(e)}")
raise raise
def _saveExecutionLog(self, automationId: str, executionLogs: List[Dict[str, Any]]) -> None:
"""
Save execution logs to an automation definition WITHOUT RBAC check.
This is a system-level operation: when a user executes an automation,
the execution log must be saved regardless of whether the user has
'update' permission on the AutomationDefinition. The user already
proved they have execute/read access by loading the automation.
"""
try:
self.db.recordModify(AutomationDefinition, automationId, {"executionLogs": executionLogs})
logger.debug(f"Saved execution log for automation {automationId}")
except Exception as e:
logger.warning(f"Could not save execution log for automation {automationId}: {e}")
def updateAutomationDefinition(self, automationId: str, automationData: Dict[str, Any]) -> AutomationDefinition: def updateAutomationDefinition(self, automationId: str, automationData: Dict[str, Any]) -> AutomationDefinition:
"""Updates an automation definition, then triggers sync.""" """Updates an automation definition, then triggers sync."""
try: try:

View file

@ -42,7 +42,7 @@ router = APIRouter(
@router.get("", response_model=PaginatedResponse[AutomationDefinition]) @router.get("", response_model=PaginatedResponse[AutomationDefinition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_automations( def get_automations(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -107,7 +107,7 @@ async def get_automations(
@router.post("", response_model=AutomationDefinition) @router.post("", response_model=AutomationDefinition)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_automation( def create_automation(
request: Request, request: Request,
automation: AutomationDefinition, automation: AutomationDefinition,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -128,7 +128,7 @@ async def create_automation(
) )
@router.get("/attributes", response_model=Dict[str, Any]) @router.get("/attributes", response_model=Dict[str, Any])
async def get_automation_attributes( def get_automation_attributes(
request: Request request: Request
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get attribute definitions for AutomationDefinition model""" """Get attribute definitions for AutomationDefinition model"""
@ -137,7 +137,7 @@ async def get_automation_attributes(
@router.get("/actions") @router.get("/actions")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_available_actions( def get_available_actions(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> JSONResponse: ) -> JSONResponse:
@ -230,7 +230,7 @@ async def get_available_actions(
@router.get("/{automationId}", response_model=AutomationDefinition) @router.get("/{automationId}", response_model=AutomationDefinition)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_automation( def get_automation(
request: Request, request: Request,
automationId: str = Path(..., description="Automation ID"), automationId: str = Path(..., description="Automation ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -257,7 +257,7 @@ async def get_automation(
@router.put("/{automationId}", response_model=AutomationDefinition) @router.put("/{automationId}", response_model=AutomationDefinition)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_automation( def update_automation(
request: Request, request: Request,
automationId: str = Path(..., description="Automation ID"), automationId: str = Path(..., description="Automation ID"),
automation: AutomationDefinition = Body(...), automation: AutomationDefinition = Body(...),
@ -285,7 +285,7 @@ async def update_automation(
@router.patch("/{automationId}/status") @router.patch("/{automationId}/status")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_automation_status( def update_automation_status(
request: Request, request: Request,
automationId: str = Path(..., description="Automation ID"), automationId: str = Path(..., description="Automation ID"),
active: bool = Body(..., embed=True), active: bool = Body(..., embed=True),
@ -326,7 +326,7 @@ async def update_automation_status(
@router.delete("/{automationId}") @router.delete("/{automationId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_automation( def delete_automation(
request: Request, request: Request,
automationId: str = Path(..., description="Automation ID"), automationId: str = Path(..., description="Automation ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -407,7 +407,7 @@ templateAttributes = getModelAttributeDefinitions(AutomationTemplate)
@templateRouter.get("", response_model=PaginatedResponse[AutomationTemplate]) @templateRouter.get("", response_model=PaginatedResponse[AutomationTemplate])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_db_templates( def get_db_templates(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -470,7 +470,7 @@ async def get_db_templates(
@templateRouter.get("/attributes", response_model=Dict[str, Any]) @templateRouter.get("/attributes", response_model=Dict[str, Any])
async def get_template_attributes( def get_template_attributes(
request: Request request: Request
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get attribute definitions for AutomationTemplate model""" """Get attribute definitions for AutomationTemplate model"""
@ -479,7 +479,7 @@ async def get_template_attributes(
@templateRouter.get("/{templateId}") @templateRouter.get("/{templateId}")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_db_template( def get_db_template(
request: Request, request: Request,
templateId: str = Path(..., description="Template ID"), templateId: str = Path(..., description="Template ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -511,7 +511,7 @@ async def get_db_template(
@templateRouter.post("") @templateRouter.post("")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_db_template( def create_db_template(
request: Request, request: Request,
templateData: Dict[str, Any] = Body(...), templateData: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -542,7 +542,7 @@ async def create_db_template(
@templateRouter.put("/{templateId}") @templateRouter.put("/{templateId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_db_template( def update_db_template(
request: Request, request: Request,
templateId: str = Path(..., description="Template ID"), templateId: str = Path(..., description="Template ID"),
templateData: Dict[str, Any] = Body(...), templateData: Dict[str, Any] = Body(...),
@ -574,7 +574,7 @@ async def update_db_template(
@templateRouter.delete("/{templateId}") @templateRouter.delete("/{templateId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_db_template( def delete_db_template(
request: Request, request: Request,
templateId: str = Path(..., description="Template ID"), templateId: str = Path(..., description="Template ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)

View file

@ -55,7 +55,7 @@ def _getServiceChat(context: RequestContext, instanceId: Optional[str] = None):
) )
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str: def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
""" """
Validate that the user has access to the feature instance. Validate that the user has access to the feature instance.
Returns the mandateId for the instance. Returns the mandateId for the instance.
@ -124,7 +124,7 @@ async def stream_chatbot_start(
- Query parameter takes precedence if both are provided - Query parameter takes precedence if both are provided
""" """
# Validate instance access # Validate instance access
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
event_manager = get_event_manager() event_manager = get_event_manager()
@ -323,7 +323,7 @@ async def stop_chatbot(
) -> ChatWorkflow: ) -> ChatWorkflow:
"""Stops a running chatbot workflow.""" """Stops a running chatbot workflow."""
# Validate instance access # Validate instance access
await _validateInstanceAccess(instanceId, context) _validateInstanceAccess(instanceId, context)
try: try:
# Get chatbot interface with instance context # Get chatbot interface with instance context
@ -392,7 +392,7 @@ async def stop_chatbot(
# to prevent "threads" from being matched as a workflowId # to prevent "threads" from being matched as a workflowId
@router.get("/{instanceId}/threads") @router.get("/{instanceId}/threads")
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_chatbot_threads( def get_chatbot_threads(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
workflowId: Optional[str] = Query(None, description="Optional workflow ID to get details and chat data for a specific thread"), workflowId: Optional[str] = Query(None, description="Optional workflow ID to get details and chat data for a specific thread"),
@ -406,7 +406,7 @@ async def get_chatbot_threads(
- If workflowId is not provided: Returns a paginated list of all workflows - If workflowId is not provided: Returns a paginated list of all workflows
""" """
# Validate instance access # Validate instance access
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
try: try:
interfaceDbChat = _getServiceChat(context, instanceId) interfaceDbChat = _getServiceChat(context, instanceId)
@ -523,7 +523,7 @@ async def get_chatbot_threads(
# NOTE: This catch-all route MUST be defined AFTER more specific routes like /threads # NOTE: This catch-all route MUST be defined AFTER more specific routes like /threads
@router.delete("/{instanceId}/{workflowId}", response_model=Dict[str, Any]) @router.delete("/{instanceId}/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def delete_chatbot( def delete_chatbot(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
workflowId: str = Path(..., description="ID of the workflow to delete"), workflowId: str = Path(..., description="ID of the workflow to delete"),
@ -531,7 +531,7 @@ async def delete_chatbot(
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Deletes a chatbot workflow and its associated data.""" """Deletes a chatbot workflow and its associated data."""
# Validate instance access - if user has access to instance, they can delete their workflows # Validate instance access - if user has access to instance, they can delete their workflows
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
try: try:
# Get service center # Get service center

View file

@ -41,7 +41,7 @@ def _getServiceChat(context: RequestContext, featureInstanceId: str = None):
) )
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str: def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
""" """
Validate that user has access to the feature instance. Validate that user has access to the feature instance.
@ -93,7 +93,7 @@ async def start_workflow(
""" """
try: try:
# Validate access and get mandate ID # Validate access and get mandate ID
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
# Start or continue workflow # Start or continue workflow
workflow = await chatStart( workflow = await chatStart(
@ -129,7 +129,7 @@ async def stop_workflow(
"""Stops a running workflow.""" """Stops a running workflow."""
try: try:
# Validate access and get mandate ID # Validate access and get mandate ID
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
# Stop workflow (pass featureInstanceId for proper RBAC filtering) # Stop workflow (pass featureInstanceId for proper RBAC filtering)
workflow = await chatStop( workflow = await chatStop(
@ -154,7 +154,7 @@ async def stop_workflow(
# Unified Chat Data Endpoint for Polling # Unified Chat Data Endpoint for Polling
@router.get("/{instanceId}/{workflowId}/chatData") @router.get("/{instanceId}/{workflowId}/chatData")
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflow_chat_data( def get_workflow_chat_data(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature instance ID"), instanceId: str = Path(..., description="Feature instance ID"),
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
@ -167,7 +167,7 @@ async def get_workflow_chat_data(
""" """
try: try:
# Validate access # Validate access
await _validateInstanceAccess(instanceId, context) _validateInstanceAccess(instanceId, context)
# Get service with feature instance context # Get service with feature instance context
chatInterface = _getServiceChat(context, featureInstanceId=instanceId) chatInterface = _getServiceChat(context, featureInstanceId=instanceId)
@ -198,7 +198,7 @@ async def get_workflow_chat_data(
# Get workflows for this instance # Get workflows for this instance
@router.get("/{instanceId}/workflows") @router.get("/{instanceId}/workflows")
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflows( def get_workflows(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature instance ID"), instanceId: str = Path(..., description="Feature instance ID"),
page: int = Query(1, ge=1, description="Page number"), page: int = Query(1, ge=1, description="Page number"),
@ -210,7 +210,7 @@ async def get_workflows(
""" """
try: try:
# Validate access # Validate access
await _validateInstanceAccess(instanceId, context) _validateInstanceAccess(instanceId, context)
# Get service with feature instance context # Get service with feature instance context
chatInterface = _getServiceChat(context, featureInstanceId=instanceId) chatInterface = _getServiceChat(context, featureInstanceId=instanceId)

View file

@ -29,7 +29,7 @@ router = APIRouter(
@router.get("/config", response_model=DataNeutraliserConfig) @router.get("/config", response_model=DataNeutraliserConfig)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_neutralization_config( def get_neutralization_config(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> DataNeutraliserConfig: ) -> DataNeutraliserConfig:
@ -62,7 +62,7 @@ async def get_neutralization_config(
@router.post("/config", response_model=DataNeutraliserConfig) @router.post("/config", response_model=DataNeutraliserConfig)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def save_neutralization_config( def save_neutralization_config(
request: Request, request: Request,
config_data: Dict[str, Any] = Body(...), config_data: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -83,7 +83,7 @@ async def save_neutralization_config(
@router.post("/neutralize-text", response_model=Dict[str, Any]) @router.post("/neutralize-text", response_model=Dict[str, Any])
@limiter.limit("20/minute") @limiter.limit("20/minute")
async def neutralize_text( def neutralize_text(
request: Request, request: Request,
text_data: Dict[str, Any] = Body(...), text_data: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -115,7 +115,7 @@ async def neutralize_text(
@router.post("/resolve-text", response_model=Dict[str, str]) @router.post("/resolve-text", response_model=Dict[str, str])
@limiter.limit("20/minute") @limiter.limit("20/minute")
async def resolve_text( def resolve_text(
request: Request, request: Request,
text_data: Dict[str, str] = Body(...), text_data: Dict[str, str] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -146,7 +146,7 @@ async def resolve_text(
@router.get("/attributes", response_model=List[DataNeutralizerAttributes]) @router.get("/attributes", response_model=List[DataNeutralizerAttributes])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_neutralization_attributes( def get_neutralization_attributes(
request: Request, request: Request,
fileId: Optional[str] = Query(None, description="Filter by file ID"), fileId: Optional[str] = Query(None, description="Filter by file ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -199,7 +199,7 @@ async def process_sharepoint_files(
@router.post("/batch-process", response_model=Dict[str, Any]) @router.post("/batch-process", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def batch_process_files( def batch_process_files(
request: Request, request: Request,
files_data: List[Dict[str, Any]] = Body(...), files_data: List[Dict[str, Any]] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -228,7 +228,7 @@ async def batch_process_files(
@router.get("/stats", response_model=Dict[str, Any]) @router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_neutralization_stats( def get_neutralization_stats(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -248,7 +248,7 @@ async def get_neutralization_stats(
@router.delete("/attributes/{fileId}", response_model=Dict[str, str]) @router.delete("/attributes/{fileId}", response_model=Dict[str, str])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def cleanup_file_attributes( def cleanup_file_attributes(
request: Request, request: Request,
fileId: str = Path(..., description="File ID to cleanup attributes for"), fileId: str = Path(..., description="File ID to cleanup attributes for"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)

View file

@ -83,7 +83,7 @@ def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
return None return None
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str: def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
""" """
Validate that the user has access to the feature instance. Validate that the user has access to the feature instance.
Returns the mandateId for the instance. Returns the mandateId for the instance.
@ -132,14 +132,14 @@ _REALESTATE_ENTITY_MODELS = {
@router.get("/{instanceId}/attributes/{entityType}", response_model=Dict[str, Any]) @router.get("/{instanceId}/attributes/{entityType}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_entity_attributes( def get_entity_attributes(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
entityType: str = Path(..., description="Entity type (e.g., Projekt, Parzelle)"), entityType: str = Path(..., description="Entity type (e.g., Projekt, Parzelle)"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get attribute definitions for a Real Estate entity. Used by FormGeneratorTable.""" """Get attribute definitions for a Real Estate entity. Used by FormGeneratorTable."""
await _validateInstanceAccess(instanceId, context) _validateInstanceAccess(instanceId, context)
if entityType not in _REALESTATE_ENTITY_MODELS: if entityType not in _REALESTATE_ENTITY_MODELS:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
@ -163,13 +163,13 @@ async def get_entity_attributes(
@router.get("/{instanceId}/projects/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/projects/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_project_options( def get_project_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get project options for select dropdowns. Returns: [{ value, label }]""" """Get project options for select dropdowns. Returns: [{ value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -179,13 +179,13 @@ async def get_project_options(
@router.get("/{instanceId}/parcels/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/parcels/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_parcel_options( def get_parcel_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get parcel options for select dropdowns. Returns: [{ value, label }]""" """Get parcel options for select dropdowns. Returns: [{ value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -197,14 +197,14 @@ async def get_parcel_options(
@router.get("/{instanceId}/projects", response_model=PaginatedResponse[Projekt]) @router.get("/{instanceId}/projects", response_model=PaginatedResponse[Projekt])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_projects( def get_projects(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[Projekt]: ) -> PaginatedResponse[Projekt]:
"""Get all projects for a feature instance with optional pagination.""" """Get all projects for a feature instance with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -241,14 +241,14 @@ async def get_projects(
@router.get("/{instanceId}/projects/{projectId}", response_model=Projekt) @router.get("/{instanceId}/projects/{projectId}", response_model=Projekt)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_project_by_id( def get_project_by_id(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
projectId: str = Path(..., description="Project ID"), projectId: str = Path(..., description="Project ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Projekt: ) -> Projekt:
"""Get a single project by ID.""" """Get a single project by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -260,14 +260,14 @@ async def get_project_by_id(
@router.post("/{instanceId}/projects", response_model=Projekt) @router.post("/{instanceId}/projects", response_model=Projekt)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def create_project( def create_project(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: Dict[str, Any] = Body(...), data: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Projekt: ) -> Projekt:
"""Create a new project.""" """Create a new project."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -284,7 +284,7 @@ async def create_project(
@router.put("/{instanceId}/projects/{projectId}", response_model=Projekt) @router.put("/{instanceId}/projects/{projectId}", response_model=Projekt)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_project( def update_project(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
projectId: str = Path(..., description="Project ID"), projectId: str = Path(..., description="Project ID"),
@ -292,7 +292,7 @@ async def update_project(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Projekt: ) -> Projekt:
"""Update a project.""" """Update a project."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -307,14 +307,14 @@ async def update_project(
@router.delete("/{instanceId}/projects/{projectId}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{instanceId}/projects/{projectId}", status_code=status.HTTP_204_NO_CONTENT)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def delete_project( def delete_project(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
projectId: str = Path(..., description="Project ID"), projectId: str = Path(..., description="Project ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> None: ) -> None:
"""Delete a project.""" """Delete a project."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -329,14 +329,14 @@ async def delete_project(
@router.get("/{instanceId}/parcels", response_model=PaginatedResponse[Parzelle]) @router.get("/{instanceId}/parcels", response_model=PaginatedResponse[Parzelle])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_parcels( def get_parcels(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[Parzelle]: ) -> PaginatedResponse[Parzelle]:
"""Get all parcels for a feature instance with optional pagination.""" """Get all parcels for a feature instance with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -373,14 +373,14 @@ async def get_parcels(
@router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle) @router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_parcel_by_id( def get_parcel_by_id(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
parcelId: str = Path(..., description="Parcel ID"), parcelId: str = Path(..., description="Parcel ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Parzelle: ) -> Parzelle:
"""Get a single parcel by ID.""" """Get a single parcel by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -392,14 +392,14 @@ async def get_parcel_by_id(
@router.post("/{instanceId}/parcels", response_model=Parzelle) @router.post("/{instanceId}/parcels", response_model=Parzelle)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def create_parcel( def create_parcel(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: Dict[str, Any] = Body(...), data: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Parzelle: ) -> Parzelle:
"""Create a new parcel.""" """Create a new parcel."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -416,7 +416,7 @@ async def create_parcel(
@router.put("/{instanceId}/parcels/{parcelId}", response_model=Parzelle) @router.put("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_parcel( def update_parcel(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
parcelId: str = Path(..., description="Parcel ID"), parcelId: str = Path(..., description="Parcel ID"),
@ -424,7 +424,7 @@ async def update_parcel(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Parzelle: ) -> Parzelle:
"""Update a parcel.""" """Update a parcel."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -439,14 +439,14 @@ async def update_parcel(
@router.delete("/{instanceId}/parcels/{parcelId}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{instanceId}/parcels/{parcelId}", status_code=status.HTTP_204_NO_CONTENT)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def delete_parcel( def delete_parcel(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
parcelId: str = Path(..., description="Parcel ID"), parcelId: str = Path(..., description="Parcel ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> None: ) -> None:
"""Delete a parcel.""" """Delete a parcel."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface( interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId context.user, mandateId=mandateId, featureInstanceId=instanceId
) )
@ -549,7 +549,7 @@ async def process_command(
@router.get("/tables", response_model=Dict[str, Any]) @router.get("/tables", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_available_tables( def get_available_tables(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -645,7 +645,7 @@ async def get_available_tables(
@router.get("/table/{table}", response_model=PaginatedResponse[Any]) @router.get("/table/{table}", response_model=PaginatedResponse[Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_table_data( def get_table_data(
request: Request, request: Request,
table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"), table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),

View file

@ -66,7 +66,7 @@ def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
return None return None
async def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str: def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
""" """
Validate that the user has access to the feature instance. Validate that the user has access to the feature instance.
Returns the mandateId for the instance. Returns the mandateId for the instance.
@ -134,7 +134,7 @@ _TRUSTEE_ENTITY_MODELS = {
@router.get("/{instanceId}/attributes/{entityType}") @router.get("/{instanceId}/attributes/{entityType}")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_entity_attributes( def get_entity_attributes(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
entityType: str = Path(..., description="Entity type (e.g., TrusteeDocument)"), entityType: str = Path(..., description="Entity type (e.g., TrusteeDocument)"),
@ -145,7 +145,7 @@ async def get_entity_attributes(
Used by FormGeneratorTable for dynamic column generation. Used by FormGeneratorTable for dynamic column generation.
""" """
# Validate instance access # Validate instance access
await _validateInstanceAccess(instanceId, context) _validateInstanceAccess(instanceId, context)
# Check if entity type is valid # Check if entity type is valid
if entityType not in _TRUSTEE_ENTITY_MODELS: if entityType not in _TRUSTEE_ENTITY_MODELS:
@ -182,7 +182,7 @@ async def get_entity_attributes(
@router.get("/mime-types/options", response_model=List[Dict[str, Any]]) @router.get("/mime-types/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_mime_type_options( def get_mime_type_options(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -217,13 +217,13 @@ async def get_mime_type_options(
@router.get("/{instanceId}/organisations/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/organisations/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_organisation_options( def get_organisation_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get organisation options for select dropdowns. Returns: [{ value, label }]""" """Get organisation options for select dropdowns. Returns: [{ value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.getAllOrganisations(None) result = interface.getAllOrganisations(None)
items = result.items if hasattr(result, 'items') else result items = result.items if hasattr(result, 'items') else result
@ -232,13 +232,13 @@ async def get_organisation_options(
@router.get("/{instanceId}/roles/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/roles/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_role_options( def get_role_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get role options for select dropdowns. Returns: [{ value, label }]""" """Get role options for select dropdowns. Returns: [{ value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.getAllRoles(None) result = interface.getAllRoles(None)
items = result.items if hasattr(result, 'items') else result items = result.items if hasattr(result, 'items') else result
@ -247,7 +247,7 @@ async def get_role_options(
@router.get("/{instanceId}/contracts/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/contracts/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_contract_options( def get_contract_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
organisationId: Optional[str] = Query(None, description="Optional: Filter by organisation ID"), organisationId: Optional[str] = Query(None, description="Optional: Filter by organisation ID"),
@ -261,7 +261,7 @@ async def get_contract_options(
Returns: [{ value, label }] Returns: [{ value, label }]
""" """
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
if organisationId: if organisationId:
@ -277,13 +277,13 @@ async def get_contract_options(
@router.get("/{instanceId}/documents/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/documents/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_document_options( def get_document_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get document options for select dropdowns. Returns: [{ id, value, label }]""" """Get document options for select dropdowns. Returns: [{ id, value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.getAllDocuments(None) result = interface.getAllDocuments(None)
items = result.items if hasattr(result, 'items') else result items = result.items if hasattr(result, 'items') else result
@ -293,13 +293,13 @@ async def get_document_options(
@router.get("/{instanceId}/positions/options", response_model=List[Dict[str, Any]]) @router.get("/{instanceId}/positions/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_position_options( def get_position_options(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get position options for select dropdowns. Returns: [{ id, value, label }]""" """Get position options for select dropdowns. Returns: [{ id, value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.getAllPositions(None) result = interface.getAllPositions(None)
items = result.items if hasattr(result, 'items') else result items = result.items if hasattr(result, 'items') else result
@ -326,14 +326,14 @@ async def get_position_options(
@router.get("/{instanceId}/organisations", response_model=PaginatedResponse[TrusteeOrganisation]) @router.get("/{instanceId}/organisations", response_model=PaginatedResponse[TrusteeOrganisation])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_organisations( def get_organisations(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeOrganisation]: ) -> PaginatedResponse[TrusteeOrganisation]:
"""Get all organisations for a feature instance with optional pagination.""" """Get all organisations for a feature instance with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -356,14 +356,14 @@ async def get_organisations(
@router.get("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation) @router.get("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_organisation( def get_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(..., description="Organisation ID"), orgId: str = Path(..., description="Organisation ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation: ) -> TrusteeOrganisation:
"""Get a single organisation by ID.""" """Get a single organisation by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
org = interface.getOrganisation(orgId) org = interface.getOrganisation(orgId)
@ -374,14 +374,14 @@ async def get_organisation(
@router.post("/{instanceId}/organisations", response_model=TrusteeOrganisation, status_code=201) @router.post("/{instanceId}/organisations", response_model=TrusteeOrganisation, status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_organisation( def create_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeOrganisation = Body(...), data: TrusteeOrganisation = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation: ) -> TrusteeOrganisation:
"""Create a new organisation.""" """Create a new organisation."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createOrganisation(data.model_dump()) result = interface.createOrganisation(data.model_dump())
@ -392,7 +392,7 @@ async def create_organisation(
@router.put("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation) @router.put("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_organisation( def update_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(..., description="Organisation ID"), orgId: str = Path(..., description="Organisation ID"),
@ -400,7 +400,7 @@ async def update_organisation(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation: ) -> TrusteeOrganisation:
"""Update an organisation.""" """Update an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getOrganisation(orgId) existing = interface.getOrganisation(orgId)
@ -415,14 +415,14 @@ async def update_organisation(
@router.delete("/{instanceId}/organisations/{orgId}") @router.delete("/{instanceId}/organisations/{orgId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_organisation( def delete_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(..., description="Organisation ID"), orgId: str = Path(..., description="Organisation ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete an organisation.""" """Delete an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getOrganisation(orgId) existing = interface.getOrganisation(orgId)
@ -439,14 +439,14 @@ async def delete_organisation(
@router.get("/{instanceId}/roles", response_model=PaginatedResponse[TrusteeRole]) @router.get("/{instanceId}/roles", response_model=PaginatedResponse[TrusteeRole])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_roles( def get_roles(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None), pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeRole]: ) -> PaginatedResponse[TrusteeRole]:
"""Get all roles with optional pagination.""" """Get all roles with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -469,14 +469,14 @@ async def get_roles(
@router.get("/{instanceId}/roles/{roleId}", response_model=TrusteeRole) @router.get("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_role( def get_role(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole: ) -> TrusteeRole:
"""Get a single role by ID.""" """Get a single role by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
role = interface.getRole(roleId) role = interface.getRole(roleId)
@ -487,14 +487,14 @@ async def get_role(
@router.post("/{instanceId}/roles", response_model=TrusteeRole, status_code=201) @router.post("/{instanceId}/roles", response_model=TrusteeRole, status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_role( def create_role(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeRole = Body(...), data: TrusteeRole = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole: ) -> TrusteeRole:
"""Create a new role (sysadmin only).""" """Create a new role (sysadmin only)."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createRole(data.model_dump()) result = interface.createRole(data.model_dump())
@ -505,7 +505,7 @@ async def create_role(
@router.put("/{instanceId}/roles/{roleId}", response_model=TrusteeRole) @router.put("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_role( def update_role(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(...), roleId: str = Path(...),
@ -513,7 +513,7 @@ async def update_role(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole: ) -> TrusteeRole:
"""Update a role (sysadmin only).""" """Update a role (sysadmin only)."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getRole(roleId) existing = interface.getRole(roleId)
@ -528,14 +528,14 @@ async def update_role(
@router.delete("/{instanceId}/roles/{roleId}") @router.delete("/{instanceId}/roles/{roleId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_role( def delete_role(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(...), roleId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete a role (sysadmin only, fails if in use).""" """Delete a role (sysadmin only, fails if in use)."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getRole(roleId) existing = interface.getRole(roleId)
@ -552,14 +552,14 @@ async def delete_role(
@router.get("/{instanceId}/access", response_model=PaginatedResponse[TrusteeAccess]) @router.get("/{instanceId}/access", response_model=PaginatedResponse[TrusteeAccess])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_all_access( def get_all_access(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None), pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeAccess]: ) -> PaginatedResponse[TrusteeAccess]:
"""Get all access records with optional pagination.""" """Get all access records with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -582,14 +582,14 @@ async def get_all_access(
@router.get("/{instanceId}/access/{accessId}", response_model=TrusteeAccess) @router.get("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_access( def get_access(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
accessId: str = Path(...), accessId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess: ) -> TrusteeAccess:
"""Get a single access record by ID.""" """Get a single access record by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
access = interface.getAccess(accessId) access = interface.getAccess(accessId)
@ -600,14 +600,14 @@ async def get_access(
@router.get("/{instanceId}/access/organisation/{orgId}", response_model=List[TrusteeAccess]) @router.get("/{instanceId}/access/organisation/{orgId}", response_model=List[TrusteeAccess])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_access_by_organisation( def get_access_by_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(...), orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeAccess]: ) -> List[TrusteeAccess]:
"""Get all access records for an organisation.""" """Get all access records for an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getAccessByOrganisation(orgId) return interface.getAccessByOrganisation(orgId)
@ -615,14 +615,14 @@ async def get_access_by_organisation(
@router.get("/{instanceId}/access/user/{userId}", response_model=List[TrusteeAccess]) @router.get("/{instanceId}/access/user/{userId}", response_model=List[TrusteeAccess])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_access_by_user( def get_access_by_user(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
userId: str = Path(...), userId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeAccess]: ) -> List[TrusteeAccess]:
"""Get all access records for a user.""" """Get all access records for a user."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getAccessByUser(userId) return interface.getAccessByUser(userId)
@ -630,14 +630,14 @@ async def get_access_by_user(
@router.post("/{instanceId}/access", response_model=TrusteeAccess, status_code=201) @router.post("/{instanceId}/access", response_model=TrusteeAccess, status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_access( def create_access(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeAccess = Body(...), data: TrusteeAccess = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess: ) -> TrusteeAccess:
"""Create a new access record.""" """Create a new access record."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createAccess(data.model_dump()) result = interface.createAccess(data.model_dump())
@ -648,7 +648,7 @@ async def create_access(
@router.put("/{instanceId}/access/{accessId}", response_model=TrusteeAccess) @router.put("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_access( def update_access(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
accessId: str = Path(...), accessId: str = Path(...),
@ -656,7 +656,7 @@ async def update_access(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess: ) -> TrusteeAccess:
"""Update an access record.""" """Update an access record."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getAccess(accessId) existing = interface.getAccess(accessId)
@ -671,14 +671,14 @@ async def update_access(
@router.delete("/{instanceId}/access/{accessId}") @router.delete("/{instanceId}/access/{accessId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_access( def delete_access(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
accessId: str = Path(...), accessId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete an access record.""" """Delete an access record."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getAccess(accessId) existing = interface.getAccess(accessId)
@ -695,14 +695,14 @@ async def delete_access(
@router.get("/{instanceId}/contracts", response_model=PaginatedResponse[TrusteeContract]) @router.get("/{instanceId}/contracts", response_model=PaginatedResponse[TrusteeContract])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_contracts( def get_contracts(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None), pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeContract]: ) -> PaginatedResponse[TrusteeContract]:
"""Get all contracts with optional pagination.""" """Get all contracts with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -725,14 +725,14 @@ async def get_contracts(
@router.get("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract) @router.get("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_contract( def get_contract(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...), contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract: ) -> TrusteeContract:
"""Get a single contract by ID.""" """Get a single contract by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
contract = interface.getContract(contractId) contract = interface.getContract(contractId)
@ -743,14 +743,14 @@ async def get_contract(
@router.get("/{instanceId}/contracts/organisation/{orgId}", response_model=List[TrusteeContract]) @router.get("/{instanceId}/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_contracts_by_organisation( def get_contracts_by_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(...), orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeContract]: ) -> List[TrusteeContract]:
"""Get all contracts for an organisation.""" """Get all contracts for an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getContractsByOrganisation(orgId) return interface.getContractsByOrganisation(orgId)
@ -758,14 +758,14 @@ async def get_contracts_by_organisation(
@router.post("/{instanceId}/contracts", response_model=TrusteeContract, status_code=201) @router.post("/{instanceId}/contracts", response_model=TrusteeContract, status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_contract( def create_contract(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeContract = Body(...), data: TrusteeContract = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract: ) -> TrusteeContract:
"""Create a new contract.""" """Create a new contract."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createContract(data.model_dump()) result = interface.createContract(data.model_dump())
@ -776,7 +776,7 @@ async def create_contract(
@router.put("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract) @router.put("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_contract( def update_contract(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...), contractId: str = Path(...),
@ -784,7 +784,7 @@ async def update_contract(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract: ) -> TrusteeContract:
"""Update a contract (organisationId is immutable).""" """Update a contract (organisationId is immutable)."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getContract(contractId) existing = interface.getContract(contractId)
@ -799,14 +799,14 @@ async def update_contract(
@router.delete("/{instanceId}/contracts/{contractId}") @router.delete("/{instanceId}/contracts/{contractId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_contract( def delete_contract(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...), contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete a contract.""" """Delete a contract."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getContract(contractId) existing = interface.getContract(contractId)
@ -823,14 +823,14 @@ async def delete_contract(
@router.get("/{instanceId}/documents", response_model=PaginatedResponse[TrusteeDocument]) @router.get("/{instanceId}/documents", response_model=PaginatedResponse[TrusteeDocument])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_documents( def get_documents(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None), pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeDocument]: ) -> PaginatedResponse[TrusteeDocument]:
"""Get all documents (metadata only) with optional pagination.""" """Get all documents (metadata only) with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -853,14 +853,14 @@ async def get_documents(
@router.get("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument) @router.get("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_document( def get_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...), documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument: ) -> TrusteeDocument:
"""Get document metadata by ID.""" """Get document metadata by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
doc = interface.getDocument(documentId) doc = interface.getDocument(documentId)
@ -871,14 +871,14 @@ async def get_document(
@router.get("/{instanceId}/documents/{documentId}/data") @router.get("/{instanceId}/documents/{documentId}/data")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def get_document_data( def get_document_data(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...), documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
): ):
"""Download document binary data.""" """Download document binary data."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
doc = interface.getDocument(documentId) doc = interface.getDocument(documentId)
@ -898,14 +898,14 @@ async def get_document_data(
@router.get("/{instanceId}/documents/contract/{contractId}", response_model=List[TrusteeDocument]) @router.get("/{instanceId}/documents/contract/{contractId}", response_model=List[TrusteeDocument])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_documents_by_contract( def get_documents_by_contract(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...), contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeDocument]: ) -> List[TrusteeDocument]:
"""Get all documents for a contract.""" """Get all documents for a contract."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getDocumentsByContract(contractId) return interface.getDocumentsByContract(contractId)
@ -919,7 +919,7 @@ async def create_document(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument: ) -> TrusteeDocument:
"""Create a new document. Accepts JSON body with optional base64-encoded documentData.""" """Create a new document. Accepts JSON body with optional base64-encoded documentData."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
# Parse JSON body # Parse JSON body
body = await request.json() body = await request.json()
@ -959,7 +959,7 @@ async def upload_document(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument: ) -> TrusteeDocument:
"""Upload a document with multipart/form-data.""" """Upload a document with multipart/form-data."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
# Read file content # Read file content
fileContent = await file.read() fileContent = await file.read()
@ -980,7 +980,7 @@ async def upload_document(
@router.put("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument) @router.put("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_document( def update_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...), documentId: str = Path(...),
@ -988,7 +988,7 @@ async def update_document(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument: ) -> TrusteeDocument:
"""Update document metadata.""" """Update document metadata."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getDocument(documentId) existing = interface.getDocument(documentId)
@ -1003,14 +1003,14 @@ async def update_document(
@router.delete("/{instanceId}/documents/{documentId}") @router.delete("/{instanceId}/documents/{documentId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_document( def delete_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...), documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete a document.""" """Delete a document."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getDocument(documentId) existing = interface.getDocument(documentId)
@ -1027,14 +1027,14 @@ async def delete_document(
@router.get("/{instanceId}/positions", response_model=PaginatedResponse[TrusteePosition]) @router.get("/{instanceId}/positions", response_model=PaginatedResponse[TrusteePosition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_positions( def get_positions(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None), pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteePosition]: ) -> PaginatedResponse[TrusteePosition]:
"""Get all positions with optional pagination.""" """Get all positions with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -1057,14 +1057,14 @@ async def get_positions(
@router.get("/{instanceId}/positions/{positionId}", response_model=TrusteePosition) @router.get("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_position( def get_position(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...), positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition: ) -> TrusteePosition:
"""Get a single position by ID.""" """Get a single position by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
position = interface.getPosition(positionId) position = interface.getPosition(positionId)
@ -1075,14 +1075,14 @@ async def get_position(
@router.get("/{instanceId}/positions/contract/{contractId}", response_model=List[TrusteePosition]) @router.get("/{instanceId}/positions/contract/{contractId}", response_model=List[TrusteePosition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_positions_by_contract( def get_positions_by_contract(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...), contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePosition]: ) -> List[TrusteePosition]:
"""Get all positions for a contract.""" """Get all positions for a contract."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getPositionsByContract(contractId) return interface.getPositionsByContract(contractId)
@ -1090,14 +1090,14 @@ async def get_positions_by_contract(
@router.get("/{instanceId}/positions/organisation/{orgId}", response_model=List[TrusteePosition]) @router.get("/{instanceId}/positions/organisation/{orgId}", response_model=List[TrusteePosition])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_positions_by_organisation( def get_positions_by_organisation(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(...), orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePosition]: ) -> List[TrusteePosition]:
"""Get all positions for an organisation.""" """Get all positions for an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getPositionsByOrganisation(orgId) return interface.getPositionsByOrganisation(orgId)
@ -1105,14 +1105,14 @@ async def get_positions_by_organisation(
@router.post("/{instanceId}/positions", response_model=TrusteePosition, status_code=201) @router.post("/{instanceId}/positions", response_model=TrusteePosition, status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_position( def create_position(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteePosition = Body(...), data: TrusteePosition = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition: ) -> TrusteePosition:
"""Create a new position.""" """Create a new position."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createPosition(data.model_dump()) result = interface.createPosition(data.model_dump())
@ -1123,7 +1123,7 @@ async def create_position(
@router.put("/{instanceId}/positions/{positionId}", response_model=TrusteePosition) @router.put("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_position( def update_position(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...), positionId: str = Path(...),
@ -1131,7 +1131,7 @@ async def update_position(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition: ) -> TrusteePosition:
"""Update a position.""" """Update a position."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getPosition(positionId) existing = interface.getPosition(positionId)
@ -1146,14 +1146,14 @@ async def update_position(
@router.delete("/{instanceId}/positions/{positionId}") @router.delete("/{instanceId}/positions/{positionId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_position( def delete_position(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...), positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete a position.""" """Delete a position."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getPosition(positionId) existing = interface.getPosition(positionId)
@ -1170,7 +1170,7 @@ async def delete_position(
@router.get("/{instanceId}/position-documents") @router.get("/{instanceId}/position-documents")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_position_documents( def get_position_documents(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None), pagination: Optional[str] = Query(None),
@ -1180,7 +1180,7 @@ async def get_position_documents(
Each item includes _permissions: { canUpdate, canDelete } for row-level permission UI. Each item includes _permissions: { canUpdate, canDelete } for row-level permission UI.
""" """
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination) paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -1203,14 +1203,14 @@ async def get_position_documents(
@router.get("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument) @router.get("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_position_document( def get_position_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
linkId: str = Path(...), linkId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument: ) -> TrusteePositionDocument:
"""Get a single position-document link by ID.""" """Get a single position-document link by ID."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
link = interface.getPositionDocument(linkId) link = interface.getPositionDocument(linkId)
@ -1221,14 +1221,14 @@ async def get_position_document(
@router.get("/{instanceId}/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument]) @router.get("/{instanceId}/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_documents_for_position( def get_documents_for_position(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...), positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePositionDocument]: ) -> List[TrusteePositionDocument]:
"""Get all document links for a position.""" """Get all document links for a position."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getDocumentsForPosition(positionId) return interface.getDocumentsForPosition(positionId)
@ -1236,14 +1236,14 @@ async def get_documents_for_position(
@router.get("/{instanceId}/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument]) @router.get("/{instanceId}/position-documents/document/{documentId}", response_model=List[TrusteePositionDocument])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_positions_for_document( def get_positions_for_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...), documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePositionDocument]: ) -> List[TrusteePositionDocument]:
"""Get all position links for a document.""" """Get all position links for a document."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getPositionsForDocument(documentId) return interface.getPositionsForDocument(documentId)
@ -1251,14 +1251,14 @@ async def get_positions_for_document(
@router.post("/{instanceId}/position-documents", response_model=TrusteePositionDocument, status_code=201) @router.post("/{instanceId}/position-documents", response_model=TrusteePositionDocument, status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_position_document( def create_position_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteePositionDocument = Body(...), data: TrusteePositionDocument = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument: ) -> TrusteePositionDocument:
"""Create a new position-document link.""" """Create a new position-document link."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createPositionDocument(data.model_dump()) result = interface.createPositionDocument(data.model_dump())
@ -1269,7 +1269,7 @@ async def create_position_document(
@router.put("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument) @router.put("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_position_document( def update_position_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
linkId: str = Path(...), linkId: str = Path(...),
@ -1277,7 +1277,7 @@ async def update_position_document(
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument: ) -> TrusteePositionDocument:
"""Update a position-document link.""" """Update a position-document link."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.updatePositionDocument(linkId, data.model_dump(exclude_unset=True)) result = interface.updatePositionDocument(linkId, data.model_dump(exclude_unset=True))
@ -1288,14 +1288,14 @@ async def update_position_document(
@router.delete("/{instanceId}/position-documents/{linkId}") @router.delete("/{instanceId}/position-documents/{linkId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_position_document( def delete_position_document(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
linkId: str = Path(...), linkId: str = Path(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Delete a position-document link.""" """Delete a position-document link."""
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId) interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getPositionDocument(linkId) existing = interface.getPositionDocument(linkId)
@ -1314,14 +1314,14 @@ async def delete_position_document(
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
async def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str: def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str:
""" """
Validate that the user has admin access to the feature instance. Validate that the user has admin access to the feature instance.
Returns the mandateId if authorized. Returns the mandateId if authorized.
This checks for the RESOURCE permission 'instance-roles.manage'. This checks for the RESOURCE permission 'instance-roles.manage'.
""" """
mandateId = await _validateInstanceAccess(instanceId, context) mandateId = _validateInstanceAccess(instanceId, context)
# SysAdmin always has access # SysAdmin always has access
if context.user.isSysAdmin: if context.user.isSysAdmin:
@ -1350,7 +1350,7 @@ async def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> st
@router.get("/{instanceId}/instance-roles", response_model=PaginatedResponse) @router.get("/{instanceId}/instance-roles", response_model=PaginatedResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_instance_roles( def get_instance_roles(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -1359,7 +1359,7 @@ async def get_instance_roles(
Get all roles for this feature instance. Get all roles for this feature instance.
Requires feature admin permission. Requires feature admin permission.
""" """
mandateId = await _validateInstanceAdmin(instanceId, context) mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface() rootInterface = getRootInterface()
@ -1374,14 +1374,14 @@ async def get_instance_roles(
@router.get("/{instanceId}/instance-roles/{roleId}", response_model=Dict[str, Any]) @router.get("/{instanceId}/instance-roles/{roleId}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_instance_role( def get_instance_role(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Get a specific instance role.""" """Get a specific instance role."""
mandateId = await _validateInstanceAdmin(instanceId, context) mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface() rootInterface = getRootInterface()
role = rootInterface.getRole(roleId) role = rootInterface.getRole(roleId)
@ -1398,7 +1398,7 @@ async def get_instance_role(
@router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse) @router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_instance_role_rules( def get_instance_role_rules(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
@ -1408,7 +1408,7 @@ async def get_instance_role_rules(
Get all AccessRules for a specific instance role. Get all AccessRules for a specific instance role.
Requires feature admin permission. Requires feature admin permission.
""" """
mandateId = await _validateInstanceAdmin(instanceId, context) mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface() rootInterface = getRootInterface()
@ -1428,7 +1428,7 @@ async def get_instance_role_rules(
@router.post("/{instanceId}/instance-roles/{roleId}/rules", response_model=Dict[str, Any], status_code=201) @router.post("/{instanceId}/instance-roles/{roleId}/rules", response_model=Dict[str, Any], status_code=201)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_instance_role_rule( def create_instance_role_rule(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
@ -1439,7 +1439,7 @@ async def create_instance_role_rule(
Create a new AccessRule for an instance role. Create a new AccessRule for an instance role.
Requires feature admin permission. Requires feature admin permission.
""" """
mandateId = await _validateInstanceAdmin(instanceId, context) mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface() rootInterface = getRootInterface()
@ -1477,7 +1477,7 @@ async def create_instance_role_rule(
@router.put("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}", response_model=Dict[str, Any]) @router.put("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_instance_role_rule( def update_instance_role_rule(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
@ -1490,7 +1490,7 @@ async def update_instance_role_rule(
Only view, read, create, update, delete can be changed. Only view, read, create, update, delete can be changed.
Requires feature admin permission. Requires feature admin permission.
""" """
mandateId = await _validateInstanceAdmin(instanceId, context) mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface() rootInterface = getRootInterface()
@ -1530,7 +1530,7 @@ async def update_instance_role_rule(
@router.delete("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}") @router.delete("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_instance_role_rule( def delete_instance_role_rule(
request: Request, request: Request,
instanceId: str = Path(..., description="Feature Instance ID"), instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
@ -1541,7 +1541,7 @@ async def delete_instance_role_rule(
Delete an AccessRule for an instance role. Delete an AccessRule for an instance role.
Requires feature admin permission. Requires feature admin permission.
""" """
mandateId = await _validateInstanceAdmin(instanceId, context) mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface() rootInterface = getRootInterface()

View file

@ -33,7 +33,7 @@ router.mount(
@router.get("/") @router.get("/")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def root(request: Request) -> Dict[str, str]: def root(request: Request) -> Dict[str, str]:
"""API status endpoint""" """API status endpoint"""
# Validate required configuration values # Validate required configuration values
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS") allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
@ -51,7 +51,7 @@ async def root(request: Request) -> Dict[str, str]:
@router.get("/api/environment") @router.get("/api/environment")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_environment( def get_environment(
request: Request, currentUser: Dict[str, Any] = Depends(getCurrentUser) request: Request, currentUser: Dict[str, Any] = Depends(getCurrentUser)
) -> Dict[str, str]: ) -> Dict[str, str]:
"""Get environment configuration for frontend""" """Get environment configuration for frontend"""
@ -82,13 +82,13 @@ async def get_environment(
@router.options("/{fullPath:path}") @router.options("/{fullPath:path}")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def options_route(request: Request, fullPath: str) -> Response: def options_route(request: Request, fullPath: str) -> Response:
return Response(status_code=200) return Response(status_code=200)
@router.get("/favicon.ico") @router.get("/favicon.ico")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def favicon(request: Request) -> FileResponse: def favicon(request: Request) -> FileResponse:
favicon_path = staticFolder / "favicon.ico" favicon_path = staticFolder / "favicon.ico"
if not favicon_path.exists(): if not favicon_path.exists():
raise HTTPException(status_code=404, detail="Favicon not found") raise HTTPException(status_code=404, detail="Favicon not found")

View file

@ -33,7 +33,7 @@ router = APIRouter(
@router.get("") @router.get("")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_all_automation_events( def get_all_automation_events(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -107,7 +107,7 @@ async def sync_all_automation_events(
@router.post("/{eventId}/remove") @router.post("/{eventId}/remove")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def remove_event( def remove_event(
request: Request, request: Request,
eventId: str = Path(..., description="Event ID to remove"), eventId: str = Path(..., description="Event ID to remove"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)

View file

@ -67,7 +67,7 @@ class SyncRolesResult(BaseModel):
@router.get("/", response_model=List[Dict[str, Any]]) @router.get("/", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_features( def list_features(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -105,7 +105,7 @@ class FeaturesMyResponse(BaseModel):
@router.get("/my", response_model=FeaturesMyResponse) @router.get("/my", response_model=FeaturesMyResponse)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_my_feature_instances( def get_my_feature_instances(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> FeaturesMyResponse: ) -> FeaturesMyResponse:
@ -332,7 +332,7 @@ def _mergeAccessLevel(current: str, new: str) -> str:
@router.post("/", response_model=Dict[str, Any]) @router.post("/", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_feature( def create_feature(
request: Request, request: Request,
code: str = Query(..., description="Unique feature code"), code: str = Query(..., description="Unique feature code"),
label: Dict[str, str] = None, label: Dict[str, str] = None,
@ -387,7 +387,7 @@ async def create_feature(
@router.get("/instances", response_model=List[Dict[str, Any]]) @router.get("/instances", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_feature_instances( def list_feature_instances(
request: Request, request: Request,
featureCode: Optional[str] = Query(None, description="Filter by feature code"), featureCode: Optional[str] = Query(None, description="Filter by feature code"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -429,7 +429,7 @@ async def list_feature_instances(
@router.get("/instances/{instanceId}", response_model=Dict[str, Any]) @router.get("/instances/{instanceId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_feature_instance( def get_feature_instance(
request: Request, request: Request,
instanceId: str, instanceId: str,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -473,7 +473,7 @@ async def get_feature_instance(
@router.post("/instances", response_model=Dict[str, Any]) @router.post("/instances", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_feature_instance( def create_feature_instance(
request: Request, request: Request,
data: FeatureInstanceCreate, data: FeatureInstanceCreate,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -540,7 +540,7 @@ async def create_feature_instance(
@router.delete("/instances/{instanceId}", response_model=Dict[str, str]) @router.delete("/instances/{instanceId}", response_model=Dict[str, str])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_feature_instance( def delete_feature_instance(
request: Request, request: Request,
instanceId: str, instanceId: str,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -605,7 +605,7 @@ class FeatureInstanceUpdate(BaseModel):
@router.put("/instances/{instanceId}", response_model=Dict[str, Any]) @router.put("/instances/{instanceId}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def updateFeatureInstance( def updateFeatureInstance(
request: Request, request: Request,
instanceId: str, instanceId: str,
data: FeatureInstanceUpdate, data: FeatureInstanceUpdate,
@ -682,7 +682,7 @@ async def updateFeatureInstance(
@router.post("/instances/{instanceId}/sync-roles", response_model=SyncRolesResult) @router.post("/instances/{instanceId}/sync-roles", response_model=SyncRolesResult)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def sync_instance_roles( def sync_instance_roles(
request: Request, request: Request,
instanceId: str, instanceId: str,
addOnly: bool = Query(True, description="Only add missing roles, don't remove extras"), addOnly: bool = Query(True, description="Only add missing roles, don't remove extras"),
@ -749,7 +749,7 @@ async def sync_instance_roles(
@router.get("/templates/roles", response_model=List[Dict[str, Any]]) @router.get("/templates/roles", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_template_roles( def list_template_roles(
request: Request, request: Request,
featureCode: Optional[str] = Query(None, description="Filter by feature code"), featureCode: Optional[str] = Query(None, description="Filter by feature code"),
sysAdmin: User = Depends(requireSysAdmin) sysAdmin: User = Depends(requireSysAdmin)
@ -779,7 +779,7 @@ async def list_template_roles(
@router.post("/templates/roles", response_model=Dict[str, Any]) @router.post("/templates/roles", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_template_role( def create_template_role(
request: Request, request: Request,
roleLabel: str = Query(..., description="Role label (e.g., 'admin', 'viewer')"), roleLabel: str = Query(..., description="Role label (e.g., 'admin', 'viewer')"),
featureCode: str = Query(..., description="Feature code this role belongs to"), featureCode: str = Query(..., description="Feature code this role belongs to"),
@ -864,7 +864,7 @@ class FeatureInstanceUserUpdate(BaseModel):
@router.get("/instances/{instanceId}/users", response_model=List[FeatureInstanceUserResponse]) @router.get("/instances/{instanceId}/users", response_model=List[FeatureInstanceUserResponse])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_feature_instance_users( def list_feature_instance_users(
request: Request, request: Request,
instanceId: str, instanceId: str,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -942,7 +942,7 @@ async def list_feature_instance_users(
@router.post("/instances/{instanceId}/users", response_model=Dict[str, Any]) @router.post("/instances/{instanceId}/users", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def add_user_to_feature_instance( def add_user_to_feature_instance(
request: Request, request: Request,
instanceId: str, instanceId: str,
data: FeatureInstanceUserCreate, data: FeatureInstanceUserCreate,
@ -1043,7 +1043,7 @@ async def add_user_to_feature_instance(
@router.delete("/instances/{instanceId}/users/{userId}", response_model=Dict[str, str]) @router.delete("/instances/{instanceId}/users/{userId}", response_model=Dict[str, str])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def remove_user_from_feature_instance( def remove_user_from_feature_instance(
request: Request, request: Request,
instanceId: str, instanceId: str,
userId: str, userId: str,
@ -1121,7 +1121,7 @@ async def remove_user_from_feature_instance(
@router.put("/instances/{instanceId}/users/{userId}/roles", response_model=Dict[str, Any]) @router.put("/instances/{instanceId}/users/{userId}/roles", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_feature_instance_user_roles( def update_feature_instance_user_roles(
request: Request, request: Request,
instanceId: str, instanceId: str,
userId: str, userId: str,
@ -1216,7 +1216,7 @@ async def update_feature_instance_user_roles(
@router.get("/instances/{instanceId}/available-roles", response_model=List[Dict[str, Any]]) @router.get("/instances/{instanceId}/available-roles", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_feature_instance_available_roles( def get_feature_instance_available_roles(
request: Request, request: Request,
instanceId: str, instanceId: str,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -1280,7 +1280,7 @@ async def get_feature_instance_available_roles(
@router.get("/{featureCode}", response_model=Dict[str, Any]) @router.get("/{featureCode}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_feature( def get_feature(
request: Request, request: Request,
featureCode: str, featureCode: str,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)

View file

@ -72,7 +72,7 @@ class RbacImportResult(BaseModel):
@router.get("/export/global", response_model=RbacExportData) @router.get("/export/global", response_model=RbacExportData)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def export_global_rbac( def export_global_rbac(
request: Request, request: Request,
sysAdmin: User = Depends(requireSysAdmin) sysAdmin: User = Depends(requireSysAdmin)
) -> RbacExportData: ) -> RbacExportData:
@ -281,7 +281,7 @@ async def import_global_rbac(
@router.get("/export/mandate", response_model=RbacExportData) @router.get("/export/mandate", response_model=RbacExportData)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def export_mandate_rbac( def export_mandate_rbac(
request: Request, request: Request,
includeFeatureInstances: bool = True, includeFeatureInstances: bool = True,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)

View file

@ -68,7 +68,7 @@ router = APIRouter(
@router.get("/", response_model=List[Dict[str, Any]]) @router.get("/", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_roles( def list_roles(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -113,7 +113,7 @@ async def list_roles(
@router.get("/options", response_model=List[Dict[str, Any]]) @router.get("/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_role_options( def get_role_options(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -154,7 +154,7 @@ async def get_role_options(
@router.post("/", response_model=Dict[str, Any]) @router.post("/", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def create_role( def create_role(
request: Request, request: Request,
role: Role = Body(...), role: Role = Body(...),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -198,7 +198,7 @@ async def create_role(
@router.get("/{roleId}", response_model=Dict[str, Any]) @router.get("/{roleId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_role( def get_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -242,7 +242,7 @@ async def get_role(
@router.put("/{roleId}", response_model=Dict[str, Any]) @router.put("/{roleId}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_role( def update_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
role: Role = Body(...), role: Role = Body(...),
@ -290,7 +290,7 @@ async def update_role(
@router.delete("/{roleId}", response_model=Dict[str, str]) @router.delete("/{roleId}", response_model=Dict[str, str])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def delete_role( def delete_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -334,7 +334,7 @@ async def delete_role(
@router.get("/users", response_model=List[Dict[str, Any]]) @router.get("/users", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_users_with_roles( def list_users_with_roles(
request: Request, request: Request,
roleLabel: Optional[str] = Query(None, description="Filter by role label"), roleLabel: Optional[str] = Query(None, description="Filter by role label"),
mandateId: Optional[str] = Query(None, description="Filter by mandate ID (via UserMandate)"), mandateId: Optional[str] = Query(None, description="Filter by mandate ID (via UserMandate)"),
@ -396,7 +396,7 @@ async def list_users_with_roles(
@router.get("/users/{userId}", response_model=Dict[str, Any]) @router.get("/users/{userId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_user_roles( def get_user_roles(
request: Request, request: Request,
userId: str = Path(..., description="User ID"), userId: str = Path(..., description="User ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -446,7 +446,7 @@ async def get_user_roles(
@router.put("/users/{userId}/roles", response_model=Dict[str, Any]) @router.put("/users/{userId}/roles", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_user_roles( def update_user_roles(
request: Request, request: Request,
userId: str = Path(..., description="User ID"), userId: str = Path(..., description="User ID"),
newRoleLabels: List[str] = Body(..., description="List of role labels to assign"), newRoleLabels: List[str] = Body(..., description="List of role labels to assign"),
@ -540,7 +540,7 @@ async def update_user_roles(
@router.post("/users/{userId}/roles/{roleLabel}", response_model=Dict[str, Any]) @router.post("/users/{userId}/roles/{roleLabel}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def add_user_role( def add_user_role(
request: Request, request: Request,
userId: str = Path(..., description="User ID"), userId: str = Path(..., description="User ID"),
roleLabel: str = Path(..., description="Role label to add"), roleLabel: str = Path(..., description="Role label to add"),
@ -619,7 +619,7 @@ async def add_user_role(
@router.delete("/users/{userId}/roles/{roleLabel}", response_model=Dict[str, Any]) @router.delete("/users/{userId}/roles/{roleLabel}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def remove_user_role( def remove_user_role(
request: Request, request: Request,
userId: str = Path(..., description="User ID"), userId: str = Path(..., description="User ID"),
roleLabel: str = Path(..., description="Role label to remove"), roleLabel: str = Path(..., description="Role label to remove"),
@ -693,7 +693,7 @@ async def remove_user_role(
@router.get("/roles/{roleLabel}/users", response_model=List[Dict[str, Any]]) @router.get("/roles/{roleLabel}/users", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_users_with_role( def get_users_with_role(
request: Request, request: Request,
roleLabel: str = Path(..., description="Role label"), roleLabel: str = Path(..., description="Role label"),
mandateId: Optional[str] = Query(None, description="Filter by mandate ID (via UserMandate)"), mandateId: Optional[str] = Query(None, description="Filter by mandate ID (via UserMandate)"),

View file

@ -35,7 +35,7 @@ router = APIRouter(
@router.get("/permissions", response_model=UserPermissions) @router.get("/permissions", response_model=UserPermissions)
@limiter.limit("300/minute") # Raised from 60 - sidebar checks many pages individually @limiter.limit("300/minute") # Raised from 60 - sidebar checks many pages individually
async def get_permissions( def get_permissions(
request: Request, request: Request,
context: str = Query(..., description="Context type: DATA, UI, or RESOURCE"), context: str = Query(..., description="Context type: DATA, UI, or RESOURCE"),
item: Optional[str] = Query(None, description="Item identifier (table name, UI path, or resource path)"), item: Optional[str] = Query(None, description="Item identifier (table name, UI path, or resource path)"),
@ -101,7 +101,7 @@ async def get_permissions(
@router.get("/permissions/all", response_model=Dict[str, Any]) @router.get("/permissions/all", response_model=Dict[str, Any])
@limiter.limit("120/minute") # Raised from 30 - optimized endpoint for bulk permission fetch @limiter.limit("120/minute") # Raised from 30 - optimized endpoint for bulk permission fetch
async def get_all_permissions( def get_all_permissions(
request: Request, request: Request,
context: Optional[str] = Query(None, description="Context type: UI or RESOURCE (if not provided, returns both)"), context: Optional[str] = Query(None, description="Context type: UI or RESOURCE (if not provided, returns both)"),
reqContext: RequestContext = Depends(getRequestContext) reqContext: RequestContext = Depends(getRequestContext)
@ -293,7 +293,7 @@ async def get_all_permissions(
@router.get("/rules", response_model=PaginatedResponse) @router.get("/rules", response_model=PaginatedResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_access_rules( def get_access_rules(
request: Request, request: Request,
roleLabel: Optional[str] = Query(None, description="Filter by role label"), roleLabel: Optional[str] = Query(None, description="Filter by role label"),
context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"), context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"),
@ -382,7 +382,7 @@ async def get_access_rules(
@router.get("/rules/by-role/{roleId}", response_model=PaginatedResponse) @router.get("/rules/by-role/{roleId}", response_model=PaginatedResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_access_rules_by_role( def get_access_rules_by_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID to get rules for"), roleId: str = Path(..., description="Role ID to get rules for"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -420,7 +420,7 @@ async def get_access_rules_by_role(
@router.get("/rules/{ruleId}", response_model=dict) @router.get("/rules/{ruleId}", response_model=dict)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_access_rule( def get_access_rule(
request: Request, request: Request,
ruleId: str = Path(..., description="Access rule ID"), ruleId: str = Path(..., description="Access rule ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -462,7 +462,7 @@ async def get_access_rule(
@router.post("/rules", response_model=dict) @router.post("/rules", response_model=dict)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def create_access_rule( def create_access_rule(
request: Request, request: Request,
accessRuleData: dict = Body(..., description="Access rule data"), accessRuleData: dict = Body(..., description="Access rule data"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -528,7 +528,7 @@ async def create_access_rule(
@router.put("/rules/{ruleId}", response_model=dict) @router.put("/rules/{ruleId}", response_model=dict)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_access_rule( def update_access_rule(
request: Request, request: Request,
ruleId: str = Path(..., description="Access rule ID"), ruleId: str = Path(..., description="Access rule ID"),
accessRuleData: dict = Body(..., description="Updated access rule data"), accessRuleData: dict = Body(..., description="Updated access rule data"),
@ -611,7 +611,7 @@ async def update_access_rule(
@router.delete("/rules/{ruleId}") @router.delete("/rules/{ruleId}")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def delete_access_rule( def delete_access_rule(
request: Request, request: Request,
ruleId: str = Path(..., description="Access rule ID"), ruleId: str = Path(..., description="Access rule ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -669,7 +669,7 @@ async def delete_access_rule(
@router.get("/roles", response_model=PaginatedResponse) @router.get("/roles", response_model=PaginatedResponse)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_roles( def list_roles(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
includeTemplates: bool = Query(False, description="Include feature template roles"), includeTemplates: bool = Query(False, description="Include feature template roles"),
@ -838,7 +838,7 @@ async def list_roles(
@router.get("/roles/options", response_model=List[Dict[str, Any]]) @router.get("/roles/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_role_options( def get_role_options(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -879,7 +879,7 @@ async def get_role_options(
@router.post("/roles", response_model=Dict[str, Any]) @router.post("/roles", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def create_role( def create_role(
request: Request, request: Request,
role: Role = Body(...), role: Role = Body(...),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -928,7 +928,7 @@ async def create_role(
@router.get("/roles/{roleId}", response_model=Dict[str, Any]) @router.get("/roles/{roleId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_role( def get_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -975,7 +975,7 @@ async def get_role(
@router.put("/roles/{roleId}", response_model=Dict[str, Any]) @router.put("/roles/{roleId}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_role( def update_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
role: Role = Body(...), role: Role = Body(...),
@ -1028,7 +1028,7 @@ async def update_role(
@router.delete("/roles/{roleId}", response_model=Dict[str, str]) @router.delete("/roles/{roleId}", response_model=Dict[str, str])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def delete_role( def delete_role(
request: Request, request: Request,
roleId: str = Path(..., description="Role ID"), roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -1078,7 +1078,7 @@ async def delete_role(
@router.get("/catalog/objects", response_model=Dict[str, Any]) @router.get("/catalog/objects", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getCatalogObjects( def getCatalogObjects(
request: Request, request: Request,
context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"), context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"),
featureCode: Optional[str] = Query(None, description="Filter by feature code"), featureCode: Optional[str] = Query(None, description="Filter by feature code"),
@ -1170,7 +1170,7 @@ async def getCatalogObjects(
@router.get("/catalog/stats", response_model=Dict[str, Any]) @router.get("/catalog/stats", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getCatalogStats( def getCatalogStats(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -1200,7 +1200,7 @@ async def getCatalogStats(
@router.post("/cleanup/duplicate-rules", response_model=dict) @router.post("/cleanup/duplicate-rules", response_model=dict)
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def cleanup_duplicate_access_rules( def cleanup_duplicate_access_rules(
request: Request, request: Request,
dryRun: bool = Query(True, description="If true, only report duplicates without deleting"), dryRun: bool = Query(True, description="If true, only report duplicates without deleting"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)

View file

@ -69,7 +69,7 @@ def _getRoleScopePriority(scope: str) -> int:
@router.get("/users", response_model=List[Dict[str, Any]]) @router.get("/users", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def listUsersForOverview( def listUsersForOverview(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -112,7 +112,7 @@ async def listUsersForOverview(
@router.get("/{userId}", response_model=Dict[str, Any]) @router.get("/{userId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getUserAccessOverview( def getUserAccessOverview(
request: Request, request: Request,
userId: str = Path(..., description="User ID to get access overview for"), userId: str = Path(..., description="User ID to get access overview for"),
mandateId: Optional[str] = Query(None, description="Filter by mandate ID"), mandateId: Optional[str] = Query(None, description="Filter by mandate ID"),
@ -410,7 +410,7 @@ async def getUserAccessOverview(
@router.get("/{userId}/effective-permissions", response_model=Dict[str, Any]) @router.get("/{userId}/effective-permissions", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getEffectivePermissions( def getEffectivePermissions(
request: Request, request: Request,
userId: str = Path(..., description="User ID"), userId: str = Path(..., description="User ID"),
mandateId: str = Query(..., description="Mandate ID context"), mandateId: str = Query(..., description="Mandate ID context"),

View file

@ -22,7 +22,7 @@ router = APIRouter(
@router.get("/{entityType}", response_model=AttributeResponse) @router.get("/{entityType}", response_model=AttributeResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_entity_attributes( def get_entity_attributes(
request: Request, request: Request,
entityType: str = Path(..., description="Type of entity (e.g. prompt)") entityType: str = Path(..., description="Type of entity (e.g. prompt)")
) -> AttributeResponse: ) -> AttributeResponse:
@ -76,7 +76,7 @@ async def get_entity_attributes(
@router.options("/{entityType}") @router.options("/{entityType}")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def options_entity_attributes( def options_entity_attributes(
request: Request, request: Request,
entityType: str = Path(..., description="Type of entity (e.g. prompt)") entityType: str = Path(..., description="Type of entity (e.g. prompt)")
) -> Response: ) -> Response:

View file

@ -164,7 +164,7 @@ router = APIRouter(
@router.get("/balance", response_model=List[BillingBalanceResponse]) @router.get("/balance", response_model=List[BillingBalanceResponse])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getBalance( def getBalance(
request: Request, request: Request,
ctx: RequestContext = Depends(getRequestContext) ctx: RequestContext = Depends(getRequestContext)
): ):
@ -189,7 +189,7 @@ async def getBalance(
@router.get("/balance/{targetMandateId}", response_model=BillingBalanceResponse) @router.get("/balance/{targetMandateId}", response_model=BillingBalanceResponse)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getBalanceForMandate( def getBalanceForMandate(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
ctx: RequestContext = Depends(getRequestContext) ctx: RequestContext = Depends(getRequestContext)
@ -230,7 +230,7 @@ async def getBalanceForMandate(
@router.get("/transactions", response_model=List[TransactionResponse]) @router.get("/transactions", response_model=List[TransactionResponse])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getTransactions( def getTransactions(
request: Request, request: Request,
limit: int = Query(default=50, ge=1, le=500), limit: int = Query(default=50, ge=1, le=500),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
@ -276,7 +276,7 @@ async def getTransactions(
@router.get("/statistics/{period}", response_model=UsageReportResponse) @router.get("/statistics/{period}", response_model=UsageReportResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getStatistics( def getStatistics(
request: Request, request: Request,
period: str = Path(..., description="Period: 'day', 'month', or 'year'"), period: str = Path(..., description="Period: 'day', 'month', or 'year'"),
year: int = Query(..., description="Year"), year: int = Query(..., description="Year"),
@ -361,7 +361,7 @@ async def getStatistics(
@router.get("/providers", response_model=List[str]) @router.get("/providers", response_model=List[str])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getAllowedProviders( def getAllowedProviders(
request: Request, request: Request,
ctx: RequestContext = Depends(getRequestContext) ctx: RequestContext = Depends(getRequestContext)
): ):
@ -388,7 +388,7 @@ async def getAllowedProviders(
@router.get("/admin/settings/{targetMandateId}", response_model=Dict[str, Any]) @router.get("/admin/settings/{targetMandateId}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getSettingsAdmin( def getSettingsAdmin(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
ctx: RequestContext = Depends(getRequestContext), ctx: RequestContext = Depends(getRequestContext),
@ -415,7 +415,7 @@ async def getSettingsAdmin(
@router.post("/admin/settings/{targetMandateId}", response_model=Dict[str, Any]) @router.post("/admin/settings/{targetMandateId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def createOrUpdateSettings( def createOrUpdateSettings(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
settingsUpdate: BillingSettingsUpdate = Body(...), settingsUpdate: BillingSettingsUpdate = Body(...),
@ -462,7 +462,7 @@ async def createOrUpdateSettings(
@router.post("/admin/credit/{targetMandateId}", response_model=Dict[str, Any]) @router.post("/admin/credit/{targetMandateId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def addCredit( def addCredit(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
creditRequest: CreditAddRequest = Body(...), creditRequest: CreditAddRequest = Body(...),
@ -526,7 +526,7 @@ async def addCredit(
@router.get("/admin/accounts/{targetMandateId}", response_model=List[AccountSummary]) @router.get("/admin/accounts/{targetMandateId}", response_model=List[AccountSummary])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getAccounts( def getAccounts(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
ctx: RequestContext = Depends(getRequestContext), ctx: RequestContext = Depends(getRequestContext),
@ -572,7 +572,7 @@ class MandateUserSummary(BaseModel):
@router.get("/admin/users/{targetMandateId}", response_model=List[MandateUserSummary]) @router.get("/admin/users/{targetMandateId}", response_model=List[MandateUserSummary])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getUsersForMandate( def getUsersForMandate(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
ctx: RequestContext = Depends(getRequestContext), ctx: RequestContext = Depends(getRequestContext),
@ -627,7 +627,7 @@ async def getUsersForMandate(
@router.get("/admin/transactions/{targetMandateId}", response_model=List[TransactionResponse]) @router.get("/admin/transactions/{targetMandateId}", response_model=List[TransactionResponse])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getTransactionsAdmin( def getTransactionsAdmin(
request: Request, request: Request,
targetMandateId: str = Path(..., description="Mandate ID"), targetMandateId: str = Path(..., description="Mandate ID"),
limit: int = Query(default=100, ge=1, le=1000), limit: int = Query(default=100, ge=1, le=1000),
@ -669,7 +669,7 @@ async def getTransactionsAdmin(
@router.get("/view/mandates/balances", response_model=List[MandateBalanceResponse]) @router.get("/view/mandates/balances", response_model=List[MandateBalanceResponse])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getMandateViewBalances( def getMandateViewBalances(
request: Request, request: Request,
ctx: RequestContext = Depends(getRequestContext), ctx: RequestContext = Depends(getRequestContext),
_admin = Depends(requireSysAdmin) _admin = Depends(requireSysAdmin)
@ -691,7 +691,7 @@ async def getMandateViewBalances(
@router.get("/view/mandates/transactions", response_model=List[TransactionResponse]) @router.get("/view/mandates/transactions", response_model=List[TransactionResponse])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getMandateViewTransactions( def getMandateViewTransactions(
request: Request, request: Request,
limit: int = Query(default=100, ge=1, le=1000), limit: int = Query(default=100, ge=1, le=1000),
ctx: RequestContext = Depends(getRequestContext), ctx: RequestContext = Depends(getRequestContext),
@ -734,7 +734,7 @@ async def getMandateViewTransactions(
@router.get("/view/users/balances", response_model=List[UserBalanceResponse]) @router.get("/view/users/balances", response_model=List[UserBalanceResponse])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getUserViewBalances( def getUserViewBalances(
request: Request, request: Request,
ctx: RequestContext = Depends(getRequestContext) ctx: RequestContext = Depends(getRequestContext)
): ):
@ -793,7 +793,7 @@ class ViewStatisticsResponse(BaseModel):
@router.get("/view/statistics") @router.get("/view/statistics")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getUserViewStatistics( def getUserViewStatistics(
request: Request, request: Request,
period: str = Query(default="month", description="Period: 'day' or 'month'"), period: str = Query(default="month", description="Period: 'day' or 'month'"),
year: int = Query(default=None, description="Year"), year: int = Query(default=None, description="Year"),
@ -962,7 +962,7 @@ async def getUserViewStatistics(
@router.get("/view/users/transactions", response_model=PaginatedResponse[UserTransactionResponse]) @router.get("/view/users/transactions", response_model=PaginatedResponse[UserTransactionResponse])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def getUserViewTransactions( def getUserViewTransactions(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
ctx: RequestContext = Depends(getRequestContext) ctx: RequestContext = Depends(getRequestContext)

View file

@ -84,7 +84,7 @@ router = APIRouter(
@router.get("/statuses/options", response_model=List[Dict[str, Any]]) @router.get("/statuses/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_connection_status_options( def get_connection_status_options(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -100,7 +100,7 @@ async def get_connection_status_options(
@router.get("/authorities/options", response_model=List[Dict[str, Any]]) @router.get("/authorities/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_auth_authority_options( def get_auth_authority_options(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -288,7 +288,7 @@ async def get_connections(
@router.post("/", response_model=UserConnection) @router.post("/", response_model=UserConnection)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_connection( def create_connection(
request: Request, request: Request,
connection_data: Dict[str, Any] = Body(...), connection_data: Dict[str, Any] = Body(...),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -344,7 +344,7 @@ async def create_connection(
@router.put("/{connectionId}", response_model=UserConnection) @router.put("/{connectionId}", response_model=UserConnection)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_connection( def update_connection(
request: Request, request: Request,
connectionId: str = Path(..., description="The ID of the connection to update"), connectionId: str = Path(..., description="The ID of the connection to update"),
connection_data: Dict[str, Any] = Body(...), connection_data: Dict[str, Any] = Body(...),
@ -416,7 +416,7 @@ async def update_connection(
@router.post("/{connectionId}/connect") @router.post("/{connectionId}/connect")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def connect_service( def connect_service(
request: Request, request: Request,
connectionId: str = Path(..., description="The ID of the connection to connect"), connectionId: str = Path(..., description="The ID of the connection to connect"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -482,7 +482,7 @@ async def connect_service(
@router.post("/{connectionId}/disconnect") @router.post("/{connectionId}/disconnect")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def disconnect_service( def disconnect_service(
request: Request, request: Request,
connectionId: str = Path(..., description="The ID of the connection to disconnect"), connectionId: str = Path(..., description="The ID of the connection to disconnect"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -532,7 +532,7 @@ async def disconnect_service(
@router.delete("/{connectionId}") @router.delete("/{connectionId}")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_connection( def delete_connection(
request: Request, request: Request,
connectionId: str = Path(..., description="The ID of the connection to delete"), connectionId: str = Path(..., description="The ID of the connection to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)

View file

@ -37,7 +37,7 @@ router = APIRouter(
@router.get("/list", response_model=PaginatedResponse[FileItem]) @router.get("/list", response_model=PaginatedResponse[FileItem])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_files( def get_files(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -168,7 +168,7 @@ async def upload_file(
@router.get("/{fileId}", response_model=FileItem) @router.get("/{fileId}", response_model=FileItem)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_file( def get_file(
request: Request, request: Request,
fileId: str = Path(..., description="ID of the file"), fileId: str = Path(..., description="ID of the file"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -214,7 +214,7 @@ async def get_file(
@router.put("/{fileId}", response_model=FileItem) @router.put("/{fileId}", response_model=FileItem)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_file( def update_file(
request: Request, request: Request,
fileId: str = Path(..., description="ID of the file to update"), fileId: str = Path(..., description="ID of the file to update"),
file_info: Dict[str, Any] = Body(...), file_info: Dict[str, Any] = Body(...),
@ -262,7 +262,7 @@ async def update_file(
@router.delete("/{fileId}", response_model=Dict[str, Any]) @router.delete("/{fileId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_file( def delete_file(
request: Request, request: Request,
fileId: str = Path(..., description="ID of the file to delete"), fileId: str = Path(..., description="ID of the file to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -289,7 +289,7 @@ async def delete_file(
@router.get("/stats", response_model=Dict[str, Any]) @router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_file_stats( def get_file_stats(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -327,7 +327,7 @@ async def get_file_stats(
@router.get("/{fileId}/download") @router.get("/{fileId}/download")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def download_file( def download_file(
request: Request, request: Request,
fileId: str = Path(..., description="ID of the file to download"), fileId: str = Path(..., description="ID of the file to download"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -375,7 +375,7 @@ async def download_file(
@router.get("/{fileId}/preview", response_model=FilePreview) @router.get("/{fileId}/preview", response_model=FilePreview)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def preview_file( def preview_file(
request: Request, request: Request,
fileId: str = Path(..., description="ID of the file to preview"), fileId: str = Path(..., description="ID of the file to preview"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)

View file

@ -76,7 +76,7 @@ router = APIRouter(
@router.get("/", response_model=PaginatedResponse[Mandate]) @router.get("/", response_model=PaginatedResponse[Mandate])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_mandates( def get_mandates(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -140,7 +140,7 @@ async def get_mandates(
@router.get("/{mandateId}", response_model=Mandate) @router.get("/{mandateId}", response_model=Mandate)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_mandate( def get_mandate(
request: Request, request: Request,
mandateId: str = Path(..., description="ID of the mandate"), mandateId: str = Path(..., description="ID of the mandate"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -171,7 +171,7 @@ async def get_mandate(
@router.post("/", response_model=Mandate) @router.post("/", response_model=Mandate)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_mandate( def create_mandate(
request: Request, request: Request,
mandateData: dict = Body(..., description="Mandate data with at least 'name' field"), mandateData: dict = Body(..., description="Mandate data with at least 'name' field"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -224,7 +224,7 @@ async def create_mandate(
@router.put("/{mandateId}", response_model=Mandate) @router.put("/{mandateId}", response_model=Mandate)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_mandate( def update_mandate(
request: Request, request: Request,
mandateId: str = Path(..., description="ID of the mandate to update"), mandateId: str = Path(..., description="ID of the mandate to update"),
mandateData: dict = Body(..., description="Mandate update data"), mandateData: dict = Body(..., description="Mandate update data"),
@ -270,7 +270,7 @@ async def update_mandate(
@router.delete("/{mandateId}", response_model=Dict[str, Any]) @router.delete("/{mandateId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_mandate( def delete_mandate(
request: Request, request: Request,
mandateId: str = Path(..., description="ID of the mandate to delete"), mandateId: str = Path(..., description="ID of the mandate to delete"),
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -324,7 +324,7 @@ async def delete_mandate(
@router.get("/{targetMandateId}/users") @router.get("/{targetMandateId}/users")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_mandate_users( def list_mandate_users(
request: Request, request: Request,
targetMandateId: str = Path(..., description="ID of the mandate"), targetMandateId: str = Path(..., description="ID of the mandate"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
@ -493,7 +493,7 @@ async def list_mandate_users(
@router.post("/{targetMandateId}/users", response_model=UserMandateResponse) @router.post("/{targetMandateId}/users", response_model=UserMandateResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def add_user_to_mandate( def add_user_to_mandate(
request: Request, request: Request,
targetMandateId: str = Path(..., description="ID of the mandate"), targetMandateId: str = Path(..., description="ID of the mandate"),
data: UserMandateCreate = Body(...), data: UserMandateCreate = Body(...),
@ -603,7 +603,7 @@ async def add_user_to_mandate(
@router.delete("/{targetMandateId}/users/{targetUserId}", response_model=Dict[str, str]) @router.delete("/{targetMandateId}/users/{targetUserId}", response_model=Dict[str, str])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def remove_user_from_mandate( def remove_user_from_mandate(
request: Request, request: Request,
targetMandateId: str = Path(..., description="ID of the mandate"), targetMandateId: str = Path(..., description="ID of the mandate"),
targetUserId: str = Path(..., description="ID of the user to remove"), targetUserId: str = Path(..., description="ID of the user to remove"),
@ -681,7 +681,7 @@ async def remove_user_from_mandate(
@router.put("/{targetMandateId}/users/{targetUserId}/roles", response_model=UserMandateResponse) @router.put("/{targetMandateId}/users/{targetUserId}/roles", response_model=UserMandateResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def update_user_roles_in_mandate( def update_user_roles_in_mandate(
request: Request, request: Request,
targetMandateId: str = Path(..., description="ID of the mandate"), targetMandateId: str = Path(..., description="ID of the mandate"),
targetUserId: str = Path(..., description="ID of the user"), targetUserId: str = Path(..., description="ID of the user"),

View file

@ -27,7 +27,7 @@ router = APIRouter(
@router.get("", response_model=PaginatedResponse[Prompt]) @router.get("", response_model=PaginatedResponse[Prompt])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_prompts( def get_prompts(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -83,7 +83,7 @@ async def get_prompts(
@router.post("", response_model=Prompt) @router.post("", response_model=Prompt)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_prompt( def create_prompt(
request: Request, request: Request,
prompt: Prompt, prompt: Prompt,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -98,7 +98,7 @@ async def create_prompt(
@router.get("/{promptId}", response_model=Prompt) @router.get("/{promptId}", response_model=Prompt)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_prompt( def get_prompt(
request: Request, request: Request,
promptId: str = Path(..., description="ID of the prompt"), promptId: str = Path(..., description="ID of the prompt"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -118,7 +118,7 @@ async def get_prompt(
@router.put("/{promptId}", response_model=Prompt) @router.put("/{promptId}", response_model=Prompt)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_prompt( def update_prompt(
request: Request, request: Request,
promptId: str = Path(..., description="ID of the prompt to update"), promptId: str = Path(..., description="ID of the prompt to update"),
promptData: Prompt = Body(...), promptData: Prompt = Body(...),
@ -154,7 +154,7 @@ async def update_prompt(
@router.delete("/{promptId}", response_model=Dict[str, Any]) @router.delete("/{promptId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_prompt( def delete_prompt(
request: Request, request: Request,
promptId: str = Path(..., description="ID of the prompt to delete"), promptId: str = Path(..., description="ID of the prompt to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)

View file

@ -153,7 +153,7 @@ router = APIRouter(
@router.get("/options", response_model=List[Dict[str, Any]]) @router.get("/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_user_options( def get_user_options(
request: Request, request: Request,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@ -190,7 +190,7 @@ async def get_user_options(
@router.get("/", response_model=PaginatedResponse[User]) @router.get("/", response_model=PaginatedResponse[User])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_users( def get_users(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -304,7 +304,7 @@ async def get_users(
@router.get("/{userId}", response_model=User) @router.get("/{userId}", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_user( def get_user(
request: Request, request: Request,
userId: str = Path(..., description="ID of the user"), userId: str = Path(..., description="ID of the user"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -356,7 +356,7 @@ class CreateUserRequest(BaseModel):
@router.post("", response_model=User) @router.post("", response_model=User)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def create_user( def create_user(
request: Request, request: Request,
userData: CreateUserRequest = Body(...), userData: CreateUserRequest = Body(...),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -396,7 +396,7 @@ async def create_user(
@router.put("/{userId}", response_model=User) @router.put("/{userId}", response_model=User)
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def update_user( def update_user(
request: Request, request: Request,
userId: str = Path(..., description="ID of the user to update"), userId: str = Path(..., description="ID of the user to update"),
userData: User = Body(...), userData: User = Body(...),
@ -438,7 +438,7 @@ async def update_user(
@router.post("/{userId}/reset-password") @router.post("/{userId}/reset-password")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def reset_user_password( def reset_user_password(
request: Request, request: Request,
userId: str = Path(..., description="ID of the user to reset password for"), userId: str = Path(..., description="ID of the user to reset password for"),
newPassword: str = Body(..., embed=True), newPassword: str = Body(..., embed=True),
@ -535,7 +535,7 @@ async def reset_user_password(
@router.post("/change-password") @router.post("/change-password")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def change_password( def change_password(
request: Request, request: Request,
currentPassword: str = Body(..., embed=True), currentPassword: str = Body(..., embed=True),
newPassword: str = Body(..., embed=True), newPassword: str = Body(..., embed=True),
@ -614,7 +614,7 @@ async def change_password(
@router.post("/{userId}/send-password-link") @router.post("/{userId}/send-password-link")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def send_password_link( def send_password_link(
request: Request, request: Request,
userId: str = Path(..., description="ID of the user to send password setup link"), userId: str = Path(..., description="ID of the user to send password setup link"),
frontendUrl: str = Body(..., embed=True), frontendUrl: str = Body(..., embed=True),
@ -749,7 +749,7 @@ Falls Sie diese Anforderung nicht erwartet haben, kontaktieren Sie bitte Ihren A
@router.delete("/{userId}", response_model=Dict[str, Any]) @router.delete("/{userId}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def delete_user( def delete_user(
request: Request, request: Request,
userId: str = Path(..., description="ID of the user to delete"), userId: str = Path(..., description="ID of the user to delete"),
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)

View file

@ -50,7 +50,7 @@ def getServiceChat(currentUser: User):
# Consolidated endpoint for getting all workflows # Consolidated endpoint for getting all workflows
@router.get("/", response_model=PaginatedResponse[ChatWorkflow]) @router.get("/", response_model=PaginatedResponse[ChatWorkflow])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflows( def get_workflows(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -123,7 +123,7 @@ async def get_workflows(
@router.get("/{workflowId}", response_model=ChatWorkflow) @router.get("/{workflowId}", response_model=ChatWorkflow)
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflow( def get_workflow(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -152,7 +152,7 @@ async def get_workflow(
@router.put("/{workflowId}", response_model=ChatWorkflow) @router.put("/{workflowId}", response_model=ChatWorkflow)
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def update_workflow( def update_workflow(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow to update"), workflowId: str = Path(..., description="ID of the workflow to update"),
workflowData: Dict[str, Any] = Body(...), workflowData: Dict[str, Any] = Body(...),
@ -200,7 +200,7 @@ async def update_workflow(
# API Endpoint for workflow status # API Endpoint for workflow status
@router.get("/{workflowId}/status", response_model=ChatWorkflow) @router.get("/{workflowId}/status", response_model=ChatWorkflow)
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflow_status( def get_workflow_status(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -274,7 +274,7 @@ async def stop_workflow(
# 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")
async def get_workflow_logs( def get_workflow_logs(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs (legacy selective data transfer)"), logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs (legacy selective data transfer)"),
@ -365,7 +365,7 @@ async def get_workflow_logs(
# API Endpoint for workflow messages with selective data transfer # API Endpoint for workflow messages with selective data transfer
@router.get("/{workflowId}/messages", response_model=PaginatedResponse[ChatMessage]) @router.get("/{workflowId}/messages", response_model=PaginatedResponse[ChatMessage])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_workflow_messages( def get_workflow_messages(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages (legacy selective data transfer)"), messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages (legacy selective data transfer)"),
@ -457,7 +457,7 @@ async def get_workflow_messages(
# State 11: Workflow Reset/Deletion endpoint # State 11: Workflow Reset/Deletion endpoint
@router.delete("/{workflowId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def delete_workflow( def delete_workflow(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow to delete"), workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -516,7 +516,7 @@ async def delete_workflow(
@router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def delete_workflow_message( def delete_workflow_message(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message to delete"), messageId: str = Path(..., description="ID of the message to delete"),
@ -566,7 +566,7 @@ async def delete_workflow_message(
@router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any]) @router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def delete_file_from_message( def delete_file_from_message(
request: Request, request: Request,
workflowId: str = Path(..., description="ID of the workflow"), workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message"), messageId: str = Path(..., description="ID of the message"),
@ -615,7 +615,7 @@ async def delete_file_from_message(
@router.get("/actions", response_model=Dict[str, Any]) @router.get("/actions", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_all_actions( def get_all_actions(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -685,7 +685,7 @@ async def get_all_actions(
@router.get("/actions/{method}", response_model=Dict[str, Any]) @router.get("/actions/{method}", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_method_actions( def get_method_actions(
request: Request, request: Request,
method: str = Path(..., description="Method name (e.g., 'outlook', 'sharepoint')"), method: str = Path(..., description="Method name (e.g., 'outlook', 'sharepoint')"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -768,7 +768,7 @@ async def get_method_actions(
@router.get("/actions/{method}/{action}", response_model=Dict[str, Any]) @router.get("/actions/{method}/{action}", response_model=Dict[str, Any])
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def get_action_schema( def get_action_schema(
request: Request, request: Request,
method: str = Path(..., description="Method name (e.g., 'outlook', 'sharepoint')"), method: str = Path(..., description="Method name (e.g., 'outlook', 'sharepoint')"),
action: str = Path(..., description="Action name (e.g., 'readEmails', 'uploadDocument')"), action: str = Path(..., description="Action name (e.g., 'readEmails', 'uploadDocument')"),

View file

@ -74,7 +74,7 @@ class DeletionResult(BaseModel):
@router.get("/data-export", response_model=DataExportResponse) @router.get("/data-export", response_model=DataExportResponse)
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def export_user_data( def export_user_data(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> DataExportResponse: ) -> DataExportResponse:
@ -215,7 +215,7 @@ async def export_user_data(
@router.get("/data-portability") @router.get("/data-portability")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def export_portable_data( def export_portable_data(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> JSONResponse: ) -> JSONResponse:
@ -296,7 +296,7 @@ async def export_portable_data(
@router.delete("/", response_model=DeletionResult) @router.delete("/", response_model=DeletionResult)
@limiter.limit("1/hour") @limiter.limit("1/hour")
async def delete_account( def delete_account(
request: Request, request: Request,
confirmDeletion: bool = False, confirmDeletion: bool = False,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -391,7 +391,7 @@ async def delete_account(
@router.get("/consent-info", response_model=Dict[str, Any]) @router.get("/consent-info", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_consent_info( def get_consent_info(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:

View file

@ -94,7 +94,7 @@ class InvitationValidation(BaseModel):
@router.post("/", response_model=InvitationResponse) @router.post("/", response_model=InvitationResponse)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def create_invitation( def create_invitation(
request: Request, request: Request,
data: InvitationCreate, data: InvitationCreate,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -300,7 +300,7 @@ async def create_invitation(
@router.get("/", response_model=List[Dict[str, Any]]) @router.get("/", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def list_invitations( def list_invitations(
request: Request, request: Request,
includeUsed: bool = Query(False, description="Include already used invitations"), includeUsed: bool = Query(False, description="Include already used invitations"),
includeExpired: bool = Query(False, description="Include expired invitations"), includeExpired: bool = Query(False, description="Include expired invitations"),
@ -379,7 +379,7 @@ async def list_invitations(
@router.delete("/{invitationId}", response_model=Dict[str, str]) @router.delete("/{invitationId}", response_model=Dict[str, str])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def revoke_invitation( def revoke_invitation(
request: Request, request: Request,
invitationId: str, invitationId: str,
context: RequestContext = Depends(getRequestContext) context: RequestContext = Depends(getRequestContext)
@ -458,7 +458,7 @@ async def revoke_invitation(
@router.get("/validate/{token}", response_model=InvitationValidation) @router.get("/validate/{token}", response_model=InvitationValidation)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def validate_invitation( def validate_invitation(
request: Request, request: Request,
token: str token: str
) -> InvitationValidation: ) -> InvitationValidation:
@ -562,7 +562,7 @@ async def validate_invitation(
@router.post("/accept/{token}", response_model=Dict[str, Any]) @router.post("/accept/{token}", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def accept_invitation( def accept_invitation(
request: Request, request: Request,
token: str, token: str,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)

View file

@ -38,7 +38,7 @@ router = APIRouter(
@router.get("/subscriptions", response_model=PaginatedResponse[MessagingSubscription]) @router.get("/subscriptions", response_model=PaginatedResponse[MessagingSubscription])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_subscriptions( def get_subscriptions(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -79,7 +79,7 @@ async def get_subscriptions(
@router.post("/subscriptions", response_model=MessagingSubscription) @router.post("/subscriptions", response_model=MessagingSubscription)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def create_subscription( def create_subscription(
request: Request, request: Request,
subscription: MessagingSubscription, subscription: MessagingSubscription,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -95,7 +95,7 @@ async def create_subscription(
@router.get("/subscriptions/{subscriptionId}", response_model=MessagingSubscription) @router.get("/subscriptions/{subscriptionId}", response_model=MessagingSubscription)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_subscription( def get_subscription(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"), subscriptionId: str = Path(..., description="ID of the subscription"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -115,7 +115,7 @@ async def get_subscription(
@router.put("/subscriptions/{subscriptionId}", response_model=MessagingSubscription) @router.put("/subscriptions/{subscriptionId}", response_model=MessagingSubscription)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def update_subscription( def update_subscription(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription to update"), subscriptionId: str = Path(..., description="ID of the subscription to update"),
subscriptionData: MessagingSubscription = Body(...), subscriptionData: MessagingSubscription = Body(...),
@ -145,7 +145,7 @@ async def update_subscription(
@router.delete("/subscriptions/{subscriptionId}", response_model=Dict[str, Any]) @router.delete("/subscriptions/{subscriptionId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def delete_subscription( def delete_subscription(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription to delete"), subscriptionId: str = Path(..., description="ID of the subscription to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -174,7 +174,7 @@ async def delete_subscription(
@router.get("/subscriptions/{subscriptionId}/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration]) @router.get("/subscriptions/{subscriptionId}/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_subscription_registrations( def get_subscription_registrations(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"), subscriptionId: str = Path(..., description="ID of the subscription"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
@ -219,7 +219,7 @@ async def get_subscription_registrations(
@router.post("/subscriptions/{subscriptionId}/subscribe", response_model=MessagingSubscriptionRegistration) @router.post("/subscriptions/{subscriptionId}/subscribe", response_model=MessagingSubscriptionRegistration)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def subscribe_user( def subscribe_user(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"), subscriptionId: str = Path(..., description="ID of the subscription"),
channel: MessagingChannel = Body(..., embed=True), channel: MessagingChannel = Body(..., embed=True),
@ -241,7 +241,7 @@ async def subscribe_user(
@router.delete("/subscriptions/{subscriptionId}/unsubscribe", response_model=Dict[str, Any]) @router.delete("/subscriptions/{subscriptionId}/unsubscribe", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def unsubscribe_user( def unsubscribe_user(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"), subscriptionId: str = Path(..., description="ID of the subscription"),
channel: MessagingChannel = Body(..., embed=True), channel: MessagingChannel = Body(..., embed=True),
@ -267,7 +267,7 @@ async def unsubscribe_user(
@router.get("/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration]) @router.get("/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_my_registrations( def get_my_registrations(
request: Request, request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -311,7 +311,7 @@ async def get_my_registrations(
@router.put("/registrations/{registrationId}", response_model=MessagingSubscriptionRegistration) @router.put("/registrations/{registrationId}", response_model=MessagingSubscriptionRegistration)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def update_registration( def update_registration(
request: Request, request: Request,
registrationId: str = Path(..., description="ID of the registration to update"), registrationId: str = Path(..., description="ID of the registration to update"),
registrationData: MessagingSubscriptionRegistration = Body(...), registrationData: MessagingSubscriptionRegistration = Body(...),
@ -341,7 +341,7 @@ async def update_registration(
@router.delete("/registrations/{registrationId}", response_model=Dict[str, Any]) @router.delete("/registrations/{registrationId}", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def delete_registration( def delete_registration(
request: Request, request: Request,
registrationId: str = Path(..., description="ID of the registration to delete"), registrationId: str = Path(..., description="ID of the registration to delete"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -376,7 +376,7 @@ def _getTriggerKey(request: Request) -> str:
@router.post("/trigger/{subscriptionId}", response_model=MessagingSubscriptionExecutionResult) @router.post("/trigger/{subscriptionId}", response_model=MessagingSubscriptionExecutionResult)
@limiter.limit("60/minute", key_func=_getTriggerKey) @limiter.limit("60/minute", key_func=_getTriggerKey)
async def trigger_subscription( def trigger_subscription(
request: Request, request: Request,
subscriptionId: str = Path(..., description="ID of the subscription to trigger"), subscriptionId: str = Path(..., description="ID of the subscription to trigger"),
eventParameters: Dict[str, Any] = Body(...), eventParameters: Dict[str, Any] = Body(...),
@ -439,7 +439,7 @@ def _hasTriggerPermission(context: RequestContext) -> bool:
@router.get("/deliveries", response_model=PaginatedResponse[MessagingDelivery]) @router.get("/deliveries", response_model=PaginatedResponse[MessagingDelivery])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_deliveries( def get_deliveries(
request: Request, request: Request,
subscriptionId: Optional[str] = Query(None, description="Filter by subscription ID"), subscriptionId: Optional[str] = Query(None, description="Filter by subscription ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"), pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
@ -485,7 +485,7 @@ async def get_deliveries(
@router.get("/deliveries/{deliveryId}", response_model=MessagingDelivery) @router.get("/deliveries/{deliveryId}", response_model=MessagingDelivery)
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_delivery( def get_delivery(
request: Request, request: Request,
deliveryId: str = Path(..., description="ID of the delivery"), deliveryId: str = Path(..., description="ID of the delivery"),
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)

View file

@ -120,7 +120,7 @@ def createInvitationNotification(
@router.get("", response_model=List[Dict[str, Any]]) @router.get("", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def getNotifications( def getNotifications(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser), currentUser: User = Depends(getCurrentUser),
status: Optional[str] = None, status: Optional[str] = None,
@ -161,7 +161,7 @@ async def getNotifications(
@router.get("/unread-count", response_model=UnreadCountResponse) @router.get("/unread-count", response_model=UnreadCountResponse)
@limiter.limit("120/minute") @limiter.limit("120/minute")
async def getUnreadCount( def getUnreadCount(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> UnreadCountResponse: ) -> UnreadCountResponse:
@ -190,7 +190,7 @@ async def getUnreadCount(
@router.put("/{notificationId}/read", response_model=Dict[str, Any]) @router.put("/{notificationId}/read", response_model=Dict[str, Any])
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def markAsRead( def markAsRead(
request: Request, request: Request,
notificationId: str, notificationId: str,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
@ -241,7 +241,7 @@ async def markAsRead(
@router.put("/mark-all-read", response_model=Dict[str, Any]) @router.put("/mark-all-read", response_model=Dict[str, Any])
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def markAllAsRead( def markAllAsRead(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -283,7 +283,7 @@ async def markAllAsRead(
@router.post("/{notificationId}/action", response_model=Dict[str, Any]) @router.post("/{notificationId}/action", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def executeAction( def executeAction(
request: Request, request: Request,
notificationId: str, notificationId: str,
actionRequest: NotificationActionRequest, actionRequest: NotificationActionRequest,
@ -332,7 +332,7 @@ async def executeAction(
actionResult = None actionResult = None
if notification.get("type") == NotificationType.INVITATION.value: if notification.get("type") == NotificationType.INVITATION.value:
actionResult = await _handleInvitationAction( actionResult = _handleInvitationAction(
notification=notification, notification=notification,
actionId=actionRequest.actionId, actionId=actionRequest.actionId,
currentUser=currentUser, currentUser=currentUser,
@ -370,7 +370,7 @@ async def executeAction(
) )
async def _handleInvitationAction( def _handleInvitationAction(
notification: Dict[str, Any], notification: Dict[str, Any],
actionId: str, actionId: str,
currentUser: User, currentUser: User,
@ -488,7 +488,7 @@ async def _handleInvitationAction(
@router.delete("/{notificationId}", response_model=Dict[str, Any]) @router.delete("/{notificationId}", response_model=Dict[str, Any])
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def deleteNotification( def deleteNotification(
request: Request, request: Request,
notificationId: str, notificationId: str,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)

View file

@ -97,7 +97,7 @@ def _getDatabaseConnector(databaseName: str, userId: str = None) -> DatabaseConn
@router.get("/tokens") @router.get("/tokens")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def list_tokens( def list_tokens(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
userId: Optional[str] = None, userId: Optional[str] = None,
@ -137,7 +137,7 @@ async def list_tokens(
@router.post("/tokens/revoke/user") @router.post("/tokens/revoke/user")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def revoke_tokens_by_user( def revoke_tokens_by_user(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
payload: Dict[str, Any] = Body(...) payload: Dict[str, Any] = Body(...)
@ -172,7 +172,7 @@ async def revoke_tokens_by_user(
@router.post("/tokens/revoke/session") @router.post("/tokens/revoke/session")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def revoke_tokens_by_session( def revoke_tokens_by_session(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
payload: Dict[str, Any] = Body(...) payload: Dict[str, Any] = Body(...)
@ -208,7 +208,7 @@ async def revoke_tokens_by_session(
@router.post("/tokens/revoke/id") @router.post("/tokens/revoke/id")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def revoke_token_by_id( def revoke_token_by_id(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
payload: Dict[str, Any] = Body(...) payload: Dict[str, Any] = Body(...)
@ -235,7 +235,7 @@ async def revoke_token_by_id(
@router.post("/tokens/revoke/mandate") @router.post("/tokens/revoke/mandate")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def revoke_tokens_by_mandate( def revoke_tokens_by_mandate(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
payload: Dict[str, Any] = Body(...) payload: Dict[str, Any] = Body(...)
@ -280,7 +280,7 @@ async def revoke_tokens_by_mandate(
@router.get("/logs/{log_name}") @router.get("/logs/{log_name}")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def download_log( def download_log(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
log_name: str = "poweron" log_name: str = "poweron"
@ -309,7 +309,7 @@ async def download_log(
@router.get("/databases") @router.get("/databases")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def list_databases( def list_databases(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -327,7 +327,7 @@ async def list_databases(
@router.get("/databases/{database_name}/tables") @router.get("/databases/{database_name}/tables")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_database_tables( def get_database_tables(
request: Request, request: Request,
database_name: str, database_name: str,
currentUser: User = Depends(requireSysAdmin) currentUser: User = Depends(requireSysAdmin)
@ -356,7 +356,7 @@ async def get_database_tables(
@router.post("/databases/{database_name}/tables/{table_name}/drop") @router.post("/databases/{database_name}/tables/{table_name}/drop")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def drop_table( def drop_table(
request: Request, request: Request,
database_name: str, database_name: str,
table_name: str, table_name: str,
@ -404,7 +404,7 @@ async def drop_table(
@router.post("/databases/drop") @router.post("/databases/drop")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def drop_database( def drop_database(
request: Request, request: Request,
currentUser: User = Depends(requireSysAdmin), currentUser: User = Depends(requireSysAdmin),
payload: Dict[str, Any] = Body(...) payload: Dict[str, Any] = Body(...)

View file

@ -93,7 +93,7 @@ SCOPES = [
] ]
@router.get("/config") @router.get("/config")
async def get_config(): def get_config():
"""Debug endpoint to check Google OAuth configuration""" """Debug endpoint to check Google OAuth configuration"""
return { return {
"client_id": CLIENT_ID, "client_id": CLIENT_ID,
@ -109,7 +109,7 @@ async def get_config():
@router.get("/login") @router.get("/login")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def login( def login(
request: Request, request: Request,
state: str = Query("login", description="State parameter to distinguish between login and connection flows"), state: str = Query("login", description="State parameter to distinguish between login and connection flows"),
connectionId: Optional[str] = Query(None, description="Connection ID for connection flow") connectionId: Optional[str] = Query(None, description="Connection ID for connection flow")
@ -589,7 +589,7 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
@router.get("/me", response_model=User) @router.get("/me", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_current_user( def get_current_user(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> User: ) -> User:
@ -605,7 +605,7 @@ async def get_current_user(
@router.post("/logout") @router.post("/logout")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def logout( def logout(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:

View file

@ -89,7 +89,7 @@ router = APIRouter(
@router.post("/login") @router.post("/login")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def login( def login(
request: Request, request: Request,
response: Response, response: Response,
formData: OAuth2PasswordRequestForm = Depends(), formData: OAuth2PasswordRequestForm = Depends(),
@ -242,7 +242,7 @@ async def login(
@router.post("/register") @router.post("/register")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def register_user( def register_user(
request: Request, request: Request,
userData: User = Body(...), userData: User = Body(...),
frontendUrl: str = Body(..., embed=True) frontendUrl: str = Body(..., embed=True)
@ -381,7 +381,7 @@ Falls Sie sich nicht registriert haben, können Sie diese E-Mail ignorieren."""
@router.get("/me", response_model=User) @router.get("/me", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def read_user_me( def read_user_me(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> User: ) -> User:
@ -397,7 +397,7 @@ async def read_user_me(
@router.post("/refresh") @router.post("/refresh")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def refresh_token( def refresh_token(
request: Request, request: Request,
response: Response response: Response
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -472,7 +472,7 @@ async def refresh_token(
@router.post("/logout") @router.post("/logout")
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def logout(request: Request, response: Response, currentUser: User = Depends(getCurrentUser)) -> JSONResponse: def logout(request: Request, response: Response, currentUser: User = Depends(getCurrentUser)) -> JSONResponse:
"""Logout from local authentication""" """Logout from local authentication"""
try: try:
# Get user interface with current user context # Get user interface with current user context
@ -541,7 +541,7 @@ async def logout(request: Request, response: Response, currentUser: User = Depen
@router.get("/available") @router.get("/available")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def check_username_availability( def check_username_availability(
request: Request, request: Request,
username: str, username: str,
authenticationAuthority: str = "local" authenticationAuthority: str = "local"
@ -573,7 +573,7 @@ async def check_username_availability(
@router.post("/password-reset-request") @router.post("/password-reset-request")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def password_reset_request( def password_reset_request(
request: Request, request: Request,
username: str = Body(..., embed=True), username: str = Body(..., embed=True),
frontendUrl: str = Body(..., embed=True) frontendUrl: str = Body(..., embed=True)
@ -653,7 +653,7 @@ Falls Sie diese Anforderung nicht gestellt haben, können Sie diese E-Mail ignor
@router.post("/password-reset") @router.post("/password-reset")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def password_reset( def password_reset(
request: Request, request: Request,
token: str = Body(..., embed=True), token: str = Body(..., embed=True),
password: str = Body(..., embed=True) password: str = Body(..., embed=True)

View file

@ -66,7 +66,7 @@ SCOPES = [
@router.get("/login") @router.get("/login")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def login( def login(
request: Request, request: Request,
state: str = Query("login", description="State parameter to distinguish between login and connection flows"), state: str = Query("login", description="State parameter to distinguish between login and connection flows"),
connectionId: Optional[str] = Query(None, description="Connection ID for connection flow") connectionId: Optional[str] = Query(None, description="Connection ID for connection flow")
@ -138,7 +138,7 @@ async def login(
@router.get("/adminconsent") @router.get("/adminconsent")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def adminconsent(request: Request) -> RedirectResponse: def adminconsent(request: Request) -> RedirectResponse:
"""Initiate Microsoft Admin Consent flow. """Initiate Microsoft Admin Consent flow.
An Azure AD admin must visit this URL once to grant consent for the entire tenant. An Azure AD admin must visit this URL once to grant consent for the entire tenant.
@ -161,7 +161,7 @@ async def adminconsent(request: Request) -> RedirectResponse:
) )
@router.get("/adminconsent/callback") @router.get("/adminconsent/callback")
async def adminconsent_callback( def adminconsent_callback(
admin_consent: Optional[str] = Query(None), admin_consent: Optional[str] = Query(None),
tenant: Optional[str] = Query(None), tenant: Optional[str] = Query(None),
error: Optional[str] = Query(None), error: Optional[str] = Query(None),
@ -603,7 +603,7 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
@router.get("/me", response_model=User) @router.get("/me", response_model=User)
@limiter.limit("30/minute") @limiter.limit("30/minute")
async def get_current_user( def get_current_user(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> User: ) -> User:
@ -619,7 +619,7 @@ async def get_current_user(
@router.post("/logout") @router.post("/logout")
@limiter.limit("10/minute") @limiter.limit("10/minute")
async def logout( def logout(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@ -655,7 +655,7 @@ async def logout(
@router.post("/cleanup") @router.post("/cleanup")
@limiter.limit("5/minute") @limiter.limit("5/minute")
async def cleanup_expired_tokens( def cleanup_expired_tokens(
request: Request, request: Request,
currentUser: User = Depends(getCurrentUser) currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]: ) -> Dict[str, Any]:

View file

@ -409,7 +409,7 @@ def _formatBlockItem(item: Dict[str, Any], language: str) -> Dict[str, Any]:
@navigationRouter.get("/navigation") @navigationRouter.get("/navigation")
@limiter.limit("60/minute") @limiter.limit("60/minute")
async def get_navigation( def get_navigation(
request: Request, request: Request,
language: str = Query("de", description="Language for labels (en, de, fr)"), language: str = Query("de", description="Language for labels (en, de, fr)"),
reqContext: RequestContext = Depends(getRequestContext) reqContext: RequestContext = Depends(getRequestContext)

View file

@ -177,17 +177,14 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
workflow = services.interfaceDbChat.updateWorkflow(workflow.id, {"name": workflowName}) workflow = services.interfaceDbChat.updateWorkflow(workflow.id, {"name": workflowName})
logger.info(f"Set workflow {workflow.id} name to: {workflowName}") logger.info(f"Set workflow {workflow.id} name to: {workflowName}")
# Update automation with execution log # Save execution log (bypasses RBAC — system operation, not a user edit)
executionLogs = list(automation.executionLogs or []) executionLogs = list(automation.executionLogs or [])
executionLogs.append(executionLog) executionLogs.append(executionLog)
# Keep only last 50 executions # Keep only last 50 executions
if len(executionLogs) > 50: if len(executionLogs) > 50:
executionLogs = executionLogs[-50:] executionLogs = executionLogs[-50:]
services.interfaceDbAutomation.updateAutomationDefinition( services.interfaceDbAutomation._saveExecutionLog(automationId, executionLogs)
automationId,
{"executionLogs": executionLogs}
)
return workflow return workflow
except Exception as e: except Exception as e:
@ -195,7 +192,7 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
executionLog["status"] = "error" executionLog["status"] = "error"
executionLog["messages"].append(f"Error: {str(e)}") executionLog["messages"].append(f"Error: {str(e)}")
# Update automation with execution log even on error # Save execution log even on error (bypasses RBAC — system operation)
try: try:
automation = services.interfaceDbAutomation.getAutomationDefinition(automationId) automation = services.interfaceDbAutomation.getAutomationDefinition(automationId)
if automation: if automation:
@ -203,10 +200,7 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
executionLogs.append(executionLog) executionLogs.append(executionLog)
if len(executionLogs) > 50: if len(executionLogs) > 50:
executionLogs = executionLogs[-50:] executionLogs = executionLogs[-50:]
services.interfaceDbAutomation.updateAutomationDefinition( services.interfaceDbAutomation._saveExecutionLog(automationId, executionLogs)
automationId,
{"executionLogs": executionLogs}
)
except Exception as logError: except Exception as logError:
logger.error(f"Error saving execution log: {str(logError)}") logger.error(f"Error saving execution log: {str(logError)}")

View file

@ -0,0 +1,377 @@
#!/usr/bin/env python3
"""
Migration Script: Convert async def def for route handlers that don't need async.
This fixes the event-loop blocking issue where synchronous psycopg2 DB operations
inside async def routes block the entire uvicorn event loop, preventing concurrent
request handling.
FastAPI behavior:
- `async def` routes run directly on the event loop (blocks if sync code inside)
- `def` routes run in a thread pool automatically (non-blocking)
Usage:
python scripts/migrate_async_to_sync.py --dry-run # Preview changes
python scripts/migrate_async_to_sync.py # Apply changes
Author: Auto-generated migration script
"""
import os
import re
import sys
import argparse
from pathlib import Path
from typing import Dict, List, Set, Tuple
# Base directory
GATEWAY_DIR = Path(__file__).parent.parent
ROUTES_DIR = GATEWAY_DIR / "modules" / "routes"
FEATURES_DIR = GATEWAY_DIR / "modules" / "features"
AUTH_DIR = GATEWAY_DIR / "modules" / "auth"
# =============================================================================
# Configuration: Functions that MUST stay async
# =============================================================================
# Key: relative file path from gateway dir
# Value: set of function names that must remain async def
_MUST_STAY_ASYNC: Dict[str, Set[str]] = {
# --- routes/ ---
"modules/routes/routeAdminAutomationEvents.py": {
"sync_all_automation_events", # await syncAutomationEvents(...)
},
"modules/routes/routeAdminRbacExport.py": {
"import_global_rbac", # await file.read()
"import_mandate_rbac", # await file.read()
},
"modules/routes/routeDataConnections.py": {
"get_connections", # await token_refresh_service.refresh_expired_tokens(...)
},
"modules/routes/routeDataFiles.py": {
"upload_file", # await file.read()
},
"modules/routes/routeDataWorkflows.py": {
"stop_workflow", # await chatStop(...)
},
# These files have many genuinely async routes (httpx, external APIs) -- keep ALL async:
"modules/routes/routeRealEstate.py": "__ALL__",
"modules/routes/routeSharepoint.py": "__ALL__",
"modules/routes/routeVoiceGoogle.py": "__ALL__",
# Partial keeps in security routes (httpx.AsyncClient, request.json()):
"modules/routes/routeSecurityGoogle.py": {
"verify_google_token", # await client.get(...)
"auth_callback", # await verify_google_token(...), await client.get(...)
"verify_token", # await verify_google_token(...)
"refresh_token", # await request.json()
},
"modules/routes/routeSecurityMsft.py": {
"auth_callback", # await client.get(...)
"refresh_token", # await request.json()
},
# --- features/ ---
"modules/features/automation/routeFeatureAutomation.py": {
"execute_automation_route", # await executeAutomation(...)
},
"modules/features/chatbot/routeFeatureChatbot.py": {
"stream_chatbot_start", # await chatProcess(...), contains async event_stream generator
"event_stream", # await request.is_disconnected(), await asyncio.wait_for(...)
"stop_chatbot", # await event_manager.emit_event(...)
},
"modules/features/chatplayground/routeFeatureChatplayground.py": {
"start_workflow", # await chatStart(...)
"stop_workflow", # await chatStop(...)
},
"modules/features/neutralization/routeFeatureNeutralizer.py": {
"process_sharepoint_files", # await service.processSharepointFiles(...)
},
"modules/features/realestate/routeFeatureRealEstate.py": {
"process_command", # await processNaturalLanguageCommand(...)
"create_table_record", # await create_project_with_parcel_data(...)
"search_parcel", # await connector.search_parcel(...), connector._query_building_layer(...)
"add_parcel_to_project", # await connector.search_parcel(...)
},
"modules/features/trustee/routeFeatureTrustee.py": {
"create_document", # await request.json()
"upload_document", # await file.read()
},
}
# Files to skip entirely (all routes must stay async)
_SKIP_FILES: Set[str] = {
"modules/routes/routeRealEstate.py",
"modules/routes/routeSharepoint.py",
"modules/routes/routeVoiceGoogle.py",
}
# Helper functions that are fake-async (async def but no await inside)
# These will be converted from async def -> def
_FAKE_ASYNC_HELPERS: Dict[str, Set[str]] = {
"modules/features/chatplayground/routeFeatureChatplayground.py": {"_validateInstanceAccess"},
"modules/features/trustee/routeFeatureTrustee.py": {"_validateInstanceAccess", "_validateInstanceAdmin"},
"modules/features/realestate/routeFeatureRealEstate.py": {"_validateInstanceAccess"},
"modules/features/chatbot/routeFeatureChatbot.py": {"_validateInstanceAccess"},
"modules/routes/routeNotifications.py": {"_handleInvitationAction"},
}
# Calls to these functions should have 'await' removed after they become sync
_REMOVE_AWAIT_CALLS: Set[str] = {
"_validateInstanceAccess",
"_validateInstanceAdmin",
"_handleInvitationAction",
}
# =============================================================================
# Migration Logic
# =============================================================================
def _getRelativePath(filePath: Path) -> str:
"""Get path relative to gateway dir."""
try:
return str(filePath.relative_to(GATEWAY_DIR)).replace("\\", "/")
except ValueError:
return str(filePath)
def _shouldSkipFile(relPath: str) -> bool:
"""Check if file should be skipped entirely."""
return relPath in _SKIP_FILES or _MUST_STAY_ASYNC.get(relPath) == "__ALL__"
def _mustStayAsync(relPath: str, funcName: str) -> bool:
"""Check if a specific function must stay async."""
keepSet = _MUST_STAY_ASYNC.get(relPath, set())
if keepSet == "__ALL__":
return True
return funcName in keepSet
def _isFakeAsyncHelper(relPath: str, funcName: str) -> bool:
"""Check if a function is a fake-async helper that should be converted."""
helpers = _FAKE_ASYNC_HELPERS.get(relPath, set())
return funcName in helpers
def _processFile(filePath: Path, dryRun: bool = True) -> Dict[str, any]:
"""Process a single file and convert async def → def where appropriate."""
relPath = _getRelativePath(filePath)
if _shouldSkipFile(relPath):
return {"file": relPath, "skipped": True, "reason": "all routes must stay async"}
with open(filePath, "r", encoding="utf-8") as f:
originalContent = f.read()
content = originalContent
changes = []
# Step 1: Find all async def functions and convert eligible ones
# Pattern matches: async def function_name(
asyncDefPattern = re.compile(r'^(\s*)async def (\w+)\s*\(', re.MULTILINE)
convertedFunctions = set()
for match in asyncDefPattern.finditer(originalContent):
indent = match.group(1)
funcName = match.group(2)
# Check if this function must stay async
if _mustStayAsync(relPath, funcName):
changes.append(f" KEEP async: {funcName} (must stay async)")
continue
# Convert async def → def
convertedFunctions.add(funcName)
changes.append(f" CONVERT: async def {funcName} -> def {funcName}")
# Apply conversions
for funcName in convertedFunctions:
# Replace "async def funcName(" with "def funcName("
# Be careful to match the exact function definition
pattern = re.compile(
r'^(\s*)async def ' + re.escape(funcName) + r'\s*\(',
re.MULTILINE
)
content = pattern.sub(
lambda m: f'{m.group(1)}def {funcName}(',
content
)
# Step 2: Remove 'await' from calls to converted functions
# This handles: await _validateInstanceAccess(...) → _validateInstanceAccess(...)
# And also: result = await someConvertedFunc(...) → result = someConvertedFunc(...)
for funcName in _REMOVE_AWAIT_CALLS:
if funcName in convertedFunctions or _isFakeAsyncHelper(relPath, funcName):
awaitPattern = re.compile(
r'(\s*)(.*)await\s+' + re.escape(funcName) + r'\s*\(',
re.MULTILINE
)
newContent = awaitPattern.sub(
lambda m: f'{m.group(1)}{m.group(2)}{funcName}(',
content
)
if newContent != content:
changes.append(f" REMOVE await: await {funcName}(...) -> {funcName}(...)")
content = newContent
# Step 3: Check for any remaining 'await' in converted functions
# This catches cases where a converted function still has await calls
remainingAwaits = []
lines = content.split('\n')
currentFunc = None
funcIndent = 0
for i, line in enumerate(lines):
# Track current function
defMatch = re.match(r'^(\s*)def (\w+)\s*\(', line)
asyncDefMatch = re.match(r'^(\s*)async def (\w+)\s*\(', line)
if defMatch and defMatch.group(2) in convertedFunctions:
currentFunc = defMatch.group(2)
funcIndent = len(defMatch.group(1))
elif defMatch or asyncDefMatch:
currentFunc = None
elif currentFunc and line.strip() and not line[0].isspace():
currentFunc = None
# Check for remaining awaits in converted functions
if currentFunc and 'await ' in line:
remainingAwaits.append(f" WARNING: Remaining 'await' in {currentFunc} at line {i+1}: {line.strip()}")
# Build result
result = {
"file": relPath,
"skipped": False,
"convertedCount": len(convertedFunctions),
"convertedFunctions": sorted(convertedFunctions),
"changes": changes,
"warnings": remainingAwaits,
"modified": content != originalContent,
}
# Write file if not dry run and content changed
if not dryRun and content != originalContent:
with open(filePath, "w", encoding="utf-8") as f:
f.write(content)
result["written"] = True
else:
result["written"] = False
return result
def _discoverRouteFiles() -> List[Path]:
"""Discover all route files to process."""
files = []
# Standard routes
if ROUTES_DIR.exists():
for f in sorted(ROUTES_DIR.glob("route*.py")):
files.append(f)
# Feature routes
if FEATURES_DIR.exists():
for f in sorted(FEATURES_DIR.glob("*/routeFeature*.py")):
files.append(f)
return files
def _main():
parser = argparse.ArgumentParser(
description="Migrate async def → def for FastAPI routes with sync DB operations"
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="Preview changes without writing files (default: apply changes)"
)
parser.add_argument(
"--file",
type=str,
default=None,
help="Process only a specific file (relative to gateway dir)"
)
args = parser.parse_args()
dryRun = args.dry_run
print("=" * 70)
print(f" FastAPI Route Migration: async def -> def")
print(f" Mode: {'DRY RUN (preview only)' if dryRun else 'APPLY CHANGES'}")
print("=" * 70)
print()
# Discover files
if args.file:
targetFile = GATEWAY_DIR / args.file.replace("/", os.sep)
if not targetFile.exists():
print(f"ERROR: File not found: {targetFile}")
sys.exit(1)
files = [targetFile]
else:
files = _discoverRouteFiles()
print(f"Found {len(files)} route files to analyze\n")
totalConverted = 0
totalWarnings = 0
totalModified = 0
allResults = []
for filePath in files:
result = _processFile(filePath, dryRun=dryRun)
allResults.append(result)
if result.get("skipped"):
print(f"[SKIP] {result['file']} - SKIPPED ({result.get('reason', '')})")
continue
converted = result.get("convertedCount", 0)
warnings = result.get("warnings", [])
modified = result.get("modified", False)
if converted == 0 and not warnings:
continue
totalConverted += converted
totalWarnings += len(warnings)
if modified:
totalModified += 1
status = "[DONE] WRITTEN" if result.get("written") else ("[PLAN] WOULD WRITE" if modified else "---")
print(f"{status} {result['file']} ({converted} functions)")
for change in result.get("changes", []):
print(f" {change}")
for warning in warnings:
print(f" [WARN] {warning}")
print()
# Summary
print("=" * 70)
print(f" SUMMARY")
print(f" Files analyzed: {len(files)}")
print(f" Files modified: {totalModified}")
print(f" Functions converted: {totalConverted}")
print(f" Warnings: {totalWarnings}")
if dryRun:
print(f"\n This was a DRY RUN. Run without --dry-run to apply changes.")
else:
print(f"\n Changes applied. Restart the server to take effect.")
print("=" * 70)
# Return exit code based on warnings
if totalWarnings > 0:
print(f"\n[WARN] There are {totalWarnings} warnings - review before deploying!")
return 1
return 0
if __name__ == "__main__":
sys.exit(_main())