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(
user=self.currentUser,
context=AccessRuleContext.DATA,
item=objectKey
item=objectKey,
mandateId=self.mandateId,
featureInstanceId=self.featureInstanceId
)
accessLevel = getattr(permissions, action, AccessLevel.NONE)
@ -373,6 +375,21 @@ class AutomationObjects:
logger.error(f"Error creating automation definition: {str(e)}")
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:
"""Updates an automation definition, then triggers sync."""
try:

View file

@ -42,7 +42,7 @@ router = APIRouter(
@router.get("", response_model=PaginatedResponse[AutomationDefinition])
@limiter.limit("30/minute")
async def get_automations(
def get_automations(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
context: RequestContext = Depends(getRequestContext)
@ -107,7 +107,7 @@ async def get_automations(
@router.post("", response_model=AutomationDefinition)
@limiter.limit("10/minute")
async def create_automation(
def create_automation(
request: Request,
automation: AutomationDefinition,
context: RequestContext = Depends(getRequestContext)
@ -128,7 +128,7 @@ async def create_automation(
)
@router.get("/attributes", response_model=Dict[str, Any])
async def get_automation_attributes(
def get_automation_attributes(
request: Request
) -> Dict[str, Any]:
"""Get attribute definitions for AutomationDefinition model"""
@ -137,7 +137,7 @@ async def get_automation_attributes(
@router.get("/actions")
@limiter.limit("30/minute")
async def get_available_actions(
def get_available_actions(
request: Request,
context: RequestContext = Depends(getRequestContext)
) -> JSONResponse:
@ -230,7 +230,7 @@ async def get_available_actions(
@router.get("/{automationId}", response_model=AutomationDefinition)
@limiter.limit("30/minute")
async def get_automation(
def get_automation(
request: Request,
automationId: str = Path(..., description="Automation ID"),
context: RequestContext = Depends(getRequestContext)
@ -257,7 +257,7 @@ async def get_automation(
@router.put("/{automationId}", response_model=AutomationDefinition)
@limiter.limit("10/minute")
async def update_automation(
def update_automation(
request: Request,
automationId: str = Path(..., description="Automation ID"),
automation: AutomationDefinition = Body(...),
@ -285,7 +285,7 @@ async def update_automation(
@router.patch("/{automationId}/status")
@limiter.limit("30/minute")
async def update_automation_status(
def update_automation_status(
request: Request,
automationId: str = Path(..., description="Automation ID"),
active: bool = Body(..., embed=True),
@ -326,7 +326,7 @@ async def update_automation_status(
@router.delete("/{automationId}")
@limiter.limit("10/minute")
async def delete_automation(
def delete_automation(
request: Request,
automationId: str = Path(..., description="Automation ID"),
context: RequestContext = Depends(getRequestContext)
@ -407,7 +407,7 @@ templateAttributes = getModelAttributeDefinitions(AutomationTemplate)
@templateRouter.get("", response_model=PaginatedResponse[AutomationTemplate])
@limiter.limit("30/minute")
async def get_db_templates(
def get_db_templates(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
context: RequestContext = Depends(getRequestContext)
@ -470,7 +470,7 @@ async def get_db_templates(
@templateRouter.get("/attributes", response_model=Dict[str, Any])
async def get_template_attributes(
def get_template_attributes(
request: Request
) -> Dict[str, Any]:
"""Get attribute definitions for AutomationTemplate model"""
@ -479,7 +479,7 @@ async def get_template_attributes(
@templateRouter.get("/{templateId}")
@limiter.limit("30/minute")
async def get_db_template(
def get_db_template(
request: Request,
templateId: str = Path(..., description="Template ID"),
context: RequestContext = Depends(getRequestContext)
@ -511,7 +511,7 @@ async def get_db_template(
@templateRouter.post("")
@limiter.limit("10/minute")
async def create_db_template(
def create_db_template(
request: Request,
templateData: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext)
@ -542,7 +542,7 @@ async def create_db_template(
@templateRouter.put("/{templateId}")
@limiter.limit("10/minute")
async def update_db_template(
def update_db_template(
request: Request,
templateId: str = Path(..., description="Template ID"),
templateData: Dict[str, Any] = Body(...),
@ -574,7 +574,7 @@ async def update_db_template(
@templateRouter.delete("/{templateId}")
@limiter.limit("10/minute")
async def delete_db_template(
def delete_db_template(
request: Request,
templateId: str = Path(..., description="Template ID"),
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.
Returns the mandateId for the instance.
@ -124,7 +124,7 @@ async def stream_chatbot_start(
- Query parameter takes precedence if both are provided
"""
# Validate instance access
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
event_manager = get_event_manager()
@ -323,7 +323,7 @@ async def stop_chatbot(
) -> ChatWorkflow:
"""Stops a running chatbot workflow."""
# Validate instance access
await _validateInstanceAccess(instanceId, context)
_validateInstanceAccess(instanceId, context)
try:
# Get chatbot interface with instance context
@ -392,7 +392,7 @@ async def stop_chatbot(
# to prevent "threads" from being matched as a workflowId
@router.get("/{instanceId}/threads")
@limiter.limit("120/minute")
async def get_chatbot_threads(
def get_chatbot_threads(
request: Request,
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"),
@ -406,7 +406,7 @@ async def get_chatbot_threads(
- If workflowId is not provided: Returns a paginated list of all workflows
"""
# Validate instance access
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
try:
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
@router.delete("/{instanceId}/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("120/minute")
async def delete_chatbot(
def delete_chatbot(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
workflowId: str = Path(..., description="ID of the workflow to delete"),
@ -531,7 +531,7 @@ async def delete_chatbot(
) -> Dict[str, Any]:
"""Deletes a chatbot workflow and its associated data."""
# Validate instance access - if user has access to instance, they can delete their workflows
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
try:
# 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.
@ -93,7 +93,7 @@ async def start_workflow(
"""
try:
# Validate access and get mandate ID
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
# Start or continue workflow
workflow = await chatStart(
@ -129,7 +129,7 @@ async def stop_workflow(
"""Stops a running workflow."""
try:
# Validate access and get mandate ID
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
# Stop workflow (pass featureInstanceId for proper RBAC filtering)
workflow = await chatStop(
@ -154,7 +154,7 @@ async def stop_workflow(
# Unified Chat Data Endpoint for Polling
@router.get("/{instanceId}/{workflowId}/chatData")
@limiter.limit("120/minute")
async def get_workflow_chat_data(
def get_workflow_chat_data(
request: Request,
instanceId: str = Path(..., description="Feature instance ID"),
workflowId: str = Path(..., description="ID of the workflow"),
@ -167,7 +167,7 @@ async def get_workflow_chat_data(
"""
try:
# Validate access
await _validateInstanceAccess(instanceId, context)
_validateInstanceAccess(instanceId, context)
# Get service with feature instance context
chatInterface = _getServiceChat(context, featureInstanceId=instanceId)
@ -198,7 +198,7 @@ async def get_workflow_chat_data(
# Get workflows for this instance
@router.get("/{instanceId}/workflows")
@limiter.limit("120/minute")
async def get_workflows(
def get_workflows(
request: Request,
instanceId: str = Path(..., description="Feature instance ID"),
page: int = Query(1, ge=1, description="Page number"),
@ -210,7 +210,7 @@ async def get_workflows(
"""
try:
# Validate access
await _validateInstanceAccess(instanceId, context)
_validateInstanceAccess(instanceId, context)
# Get service with feature instance context
chatInterface = _getServiceChat(context, featureInstanceId=instanceId)

View file

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

View file

@ -83,7 +83,7 @@ def _parsePagination(pagination: Optional[str]) -> Optional[PaginationParams]:
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.
Returns the mandateId for the instance.
@ -132,14 +132,14 @@ _REALESTATE_ENTITY_MODELS = {
@router.get("/{instanceId}/attributes/{entityType}", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def get_entity_attributes(
def get_entity_attributes(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
entityType: str = Path(..., description="Entity type (e.g., Projekt, Parzelle)"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""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:
raise HTTPException(
status_code=404,
@ -163,13 +163,13 @@ async def get_entity_attributes(
@router.get("/{instanceId}/projects/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute")
async def get_project_options(
def get_project_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""Get project options for select dropdowns. Returns: [{ value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
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]])
@limiter.limit("60/minute")
async def get_parcel_options(
def get_parcel_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""Get parcel options for select dropdowns. Returns: [{ value, label }]"""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -197,14 +197,14 @@ async def get_parcel_options(
@router.get("/{instanceId}/projects", response_model=PaginatedResponse[Projekt])
@limiter.limit("30/minute")
async def get_projects(
def get_projects(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[Projekt]:
"""Get all projects for a feature instance with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -241,14 +241,14 @@ async def get_projects(
@router.get("/{instanceId}/projects/{projectId}", response_model=Projekt)
@limiter.limit("30/minute")
async def get_project_by_id(
def get_project_by_id(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
projectId: str = Path(..., description="Project ID"),
context: RequestContext = Depends(getRequestContext)
) -> Projekt:
"""Get a single project by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -260,14 +260,14 @@ async def get_project_by_id(
@router.post("/{instanceId}/projects", response_model=Projekt)
@limiter.limit("30/minute")
async def create_project(
def create_project(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> Projekt:
"""Create a new project."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -284,7 +284,7 @@ async def create_project(
@router.put("/{instanceId}/projects/{projectId}", response_model=Projekt)
@limiter.limit("30/minute")
async def update_project(
def update_project(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
projectId: str = Path(..., description="Project ID"),
@ -292,7 +292,7 @@ async def update_project(
context: RequestContext = Depends(getRequestContext)
) -> Projekt:
"""Update a project."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
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)
@limiter.limit("30/minute")
async def delete_project(
def delete_project(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
projectId: str = Path(..., description="Project ID"),
context: RequestContext = Depends(getRequestContext)
) -> None:
"""Delete a project."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -329,14 +329,14 @@ async def delete_project(
@router.get("/{instanceId}/parcels", response_model=PaginatedResponse[Parzelle])
@limiter.limit("30/minute")
async def get_parcels(
def get_parcels(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[Parzelle]:
"""Get all parcels for a feature instance with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -373,14 +373,14 @@ async def get_parcels(
@router.get("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
@limiter.limit("30/minute")
async def get_parcel_by_id(
def get_parcel_by_id(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
parcelId: str = Path(..., description="Parcel ID"),
context: RequestContext = Depends(getRequestContext)
) -> Parzelle:
"""Get a single parcel by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -392,14 +392,14 @@ async def get_parcel_by_id(
@router.post("/{instanceId}/parcels", response_model=Parzelle)
@limiter.limit("30/minute")
async def create_parcel(
def create_parcel(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> Parzelle:
"""Create a new parcel."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -416,7 +416,7 @@ async def create_parcel(
@router.put("/{instanceId}/parcels/{parcelId}", response_model=Parzelle)
@limiter.limit("30/minute")
async def update_parcel(
def update_parcel(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
parcelId: str = Path(..., description="Parcel ID"),
@ -424,7 +424,7 @@ async def update_parcel(
context: RequestContext = Depends(getRequestContext)
) -> Parzelle:
"""Update a parcel."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
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)
@limiter.limit("30/minute")
async def delete_parcel(
def delete_parcel(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
parcelId: str = Path(..., description="Parcel ID"),
context: RequestContext = Depends(getRequestContext)
) -> None:
"""Delete a parcel."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getRealEstateInterface(
context.user, mandateId=mandateId, featureInstanceId=instanceId
)
@ -549,7 +549,7 @@ async def process_command(
@router.get("/tables", response_model=Dict[str, Any])
@limiter.limit("120/minute")
async def get_available_tables(
def get_available_tables(
request: Request,
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
@ -645,7 +645,7 @@ async def get_available_tables(
@router.get("/table/{table}", response_model=PaginatedResponse[Any])
@limiter.limit("120/minute")
async def get_table_data(
def get_table_data(
request: Request,
table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"),
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
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.
Returns the mandateId for the instance.
@ -134,7 +134,7 @@ _TRUSTEE_ENTITY_MODELS = {
@router.get("/{instanceId}/attributes/{entityType}")
@limiter.limit("30/minute")
async def get_entity_attributes(
def get_entity_attributes(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
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.
"""
# Validate instance access
await _validateInstanceAccess(instanceId, context)
_validateInstanceAccess(instanceId, context)
# Check if entity type is valid
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]])
@limiter.limit("60/minute")
async def get_mime_type_options(
def get_mime_type_options(
request: Request,
context: RequestContext = Depends(getRequestContext)
) -> 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]])
@limiter.limit("60/minute")
async def get_organisation_options(
def get_organisation_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""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)
result = interface.getAllOrganisations(None)
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]])
@limiter.limit("60/minute")
async def get_role_options(
def get_role_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""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)
result = interface.getAllRoles(None)
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]])
@limiter.limit("60/minute")
async def get_contract_options(
def get_contract_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
organisationId: Optional[str] = Query(None, description="Optional: Filter by organisation ID"),
@ -261,7 +261,7 @@ async def get_contract_options(
Returns: [{ value, label }]
"""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
if organisationId:
@ -277,13 +277,13 @@ async def get_contract_options(
@router.get("/{instanceId}/documents/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute")
async def get_document_options(
def get_document_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""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)
result = interface.getAllDocuments(None)
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]])
@limiter.limit("60/minute")
async def get_position_options(
def get_position_options(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""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)
result = interface.getAllPositions(None)
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])
@limiter.limit("30/minute")
async def get_organisations(
def get_organisations(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams"),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeOrganisation]:
"""Get all organisations for a feature instance with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -356,14 +356,14 @@ async def get_organisations(
@router.get("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
@limiter.limit("30/minute")
async def get_organisation(
def get_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(..., description="Organisation ID"),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation:
"""Get a single organisation by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
org = interface.getOrganisation(orgId)
@ -374,14 +374,14 @@ async def get_organisation(
@router.post("/{instanceId}/organisations", response_model=TrusteeOrganisation, status_code=201)
@limiter.limit("10/minute")
async def create_organisation(
def create_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeOrganisation = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation:
"""Create a new organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createOrganisation(data.model_dump())
@ -392,7 +392,7 @@ async def create_organisation(
@router.put("/{instanceId}/organisations/{orgId}", response_model=TrusteeOrganisation)
@limiter.limit("10/minute")
async def update_organisation(
def update_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(..., description="Organisation ID"),
@ -400,7 +400,7 @@ async def update_organisation(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeOrganisation:
"""Update an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getOrganisation(orgId)
@ -415,14 +415,14 @@ async def update_organisation(
@router.delete("/{instanceId}/organisations/{orgId}")
@limiter.limit("10/minute")
async def delete_organisation(
def delete_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(..., description="Organisation ID"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getOrganisation(orgId)
@ -439,14 +439,14 @@ async def delete_organisation(
@router.get("/{instanceId}/roles", response_model=PaginatedResponse[TrusteeRole])
@limiter.limit("30/minute")
async def get_roles(
def get_roles(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeRole]:
"""Get all roles with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -469,14 +469,14 @@ async def get_roles(
@router.get("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
@limiter.limit("30/minute")
async def get_role(
def get_role(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole:
"""Get a single role by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
role = interface.getRole(roleId)
@ -487,14 +487,14 @@ async def get_role(
@router.post("/{instanceId}/roles", response_model=TrusteeRole, status_code=201)
@limiter.limit("10/minute")
async def create_role(
def create_role(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeRole = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole:
"""Create a new role (sysadmin only)."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createRole(data.model_dump())
@ -505,7 +505,7 @@ async def create_role(
@router.put("/{instanceId}/roles/{roleId}", response_model=TrusteeRole)
@limiter.limit("10/minute")
async def update_role(
def update_role(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(...),
@ -513,7 +513,7 @@ async def update_role(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeRole:
"""Update a role (sysadmin only)."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getRole(roleId)
@ -528,14 +528,14 @@ async def update_role(
@router.delete("/{instanceId}/roles/{roleId}")
@limiter.limit("10/minute")
async def delete_role(
def delete_role(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""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)
existing = interface.getRole(roleId)
@ -552,14 +552,14 @@ async def delete_role(
@router.get("/{instanceId}/access", response_model=PaginatedResponse[TrusteeAccess])
@limiter.limit("30/minute")
async def get_all_access(
def get_all_access(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeAccess]:
"""Get all access records with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
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)
@limiter.limit("30/minute")
async def get_access(
def get_access(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
accessId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess:
"""Get a single access record by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
access = interface.getAccess(accessId)
@ -600,14 +600,14 @@ async def get_access(
@router.get("/{instanceId}/access/organisation/{orgId}", response_model=List[TrusteeAccess])
@limiter.limit("30/minute")
async def get_access_by_organisation(
def get_access_by_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeAccess]:
"""Get all access records for an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getAccessByOrganisation(orgId)
@ -615,14 +615,14 @@ async def get_access_by_organisation(
@router.get("/{instanceId}/access/user/{userId}", response_model=List[TrusteeAccess])
@limiter.limit("30/minute")
async def get_access_by_user(
def get_access_by_user(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
userId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeAccess]:
"""Get all access records for a user."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getAccessByUser(userId)
@ -630,14 +630,14 @@ async def get_access_by_user(
@router.post("/{instanceId}/access", response_model=TrusteeAccess, status_code=201)
@limiter.limit("10/minute")
async def create_access(
def create_access(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeAccess = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess:
"""Create a new access record."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createAccess(data.model_dump())
@ -648,7 +648,7 @@ async def create_access(
@router.put("/{instanceId}/access/{accessId}", response_model=TrusteeAccess)
@limiter.limit("10/minute")
async def update_access(
def update_access(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
accessId: str = Path(...),
@ -656,7 +656,7 @@ async def update_access(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeAccess:
"""Update an access record."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getAccess(accessId)
@ -671,14 +671,14 @@ async def update_access(
@router.delete("/{instanceId}/access/{accessId}")
@limiter.limit("10/minute")
async def delete_access(
def delete_access(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
accessId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete an access record."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getAccess(accessId)
@ -695,14 +695,14 @@ async def delete_access(
@router.get("/{instanceId}/contracts", response_model=PaginatedResponse[TrusteeContract])
@limiter.limit("30/minute")
async def get_contracts(
def get_contracts(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeContract]:
"""Get all contracts with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -725,14 +725,14 @@ async def get_contracts(
@router.get("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
@limiter.limit("30/minute")
async def get_contract(
def get_contract(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract:
"""Get a single contract by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
contract = interface.getContract(contractId)
@ -743,14 +743,14 @@ async def get_contract(
@router.get("/{instanceId}/contracts/organisation/{orgId}", response_model=List[TrusteeContract])
@limiter.limit("30/minute")
async def get_contracts_by_organisation(
def get_contracts_by_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeContract]:
"""Get all contracts for an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getContractsByOrganisation(orgId)
@ -758,14 +758,14 @@ async def get_contracts_by_organisation(
@router.post("/{instanceId}/contracts", response_model=TrusteeContract, status_code=201)
@limiter.limit("10/minute")
async def create_contract(
def create_contract(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteeContract = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract:
"""Create a new contract."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createContract(data.model_dump())
@ -776,7 +776,7 @@ async def create_contract(
@router.put("/{instanceId}/contracts/{contractId}", response_model=TrusteeContract)
@limiter.limit("10/minute")
async def update_contract(
def update_contract(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...),
@ -784,7 +784,7 @@ async def update_contract(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeContract:
"""Update a contract (organisationId is immutable)."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getContract(contractId)
@ -799,14 +799,14 @@ async def update_contract(
@router.delete("/{instanceId}/contracts/{contractId}")
@limiter.limit("10/minute")
async def delete_contract(
def delete_contract(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a contract."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getContract(contractId)
@ -823,14 +823,14 @@ async def delete_contract(
@router.get("/{instanceId}/documents", response_model=PaginatedResponse[TrusteeDocument])
@limiter.limit("30/minute")
async def get_documents(
def get_documents(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteeDocument]:
"""Get all documents (metadata only) with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -853,14 +853,14 @@ async def get_documents(
@router.get("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
@limiter.limit("30/minute")
async def get_document(
def get_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Get document metadata by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
doc = interface.getDocument(documentId)
@ -871,14 +871,14 @@ async def get_document(
@router.get("/{instanceId}/documents/{documentId}/data")
@limiter.limit("10/minute")
async def get_document_data(
def get_document_data(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
):
"""Download document binary data."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
doc = interface.getDocument(documentId)
@ -898,14 +898,14 @@ async def get_document_data(
@router.get("/{instanceId}/documents/contract/{contractId}", response_model=List[TrusteeDocument])
@limiter.limit("30/minute")
async def get_documents_by_contract(
def get_documents_by_contract(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteeDocument]:
"""Get all documents for a contract."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getDocumentsByContract(contractId)
@ -919,7 +919,7 @@ async def create_document(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Create a new document. Accepts JSON body with optional base64-encoded documentData."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
# Parse JSON body
body = await request.json()
@ -959,7 +959,7 @@ async def upload_document(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Upload a document with multipart/form-data."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
# Read file content
fileContent = await file.read()
@ -980,7 +980,7 @@ async def upload_document(
@router.put("/{instanceId}/documents/{documentId}", response_model=TrusteeDocument)
@limiter.limit("10/minute")
async def update_document(
def update_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...),
@ -988,7 +988,7 @@ async def update_document(
context: RequestContext = Depends(getRequestContext)
) -> TrusteeDocument:
"""Update document metadata."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getDocument(documentId)
@ -1003,14 +1003,14 @@ async def update_document(
@router.delete("/{instanceId}/documents/{documentId}")
@limiter.limit("10/minute")
async def delete_document(
def delete_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a document."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getDocument(documentId)
@ -1027,14 +1027,14 @@ async def delete_document(
@router.get("/{instanceId}/positions", response_model=PaginatedResponse[TrusteePosition])
@limiter.limit("30/minute")
async def get_positions(
def get_positions(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
pagination: Optional[str] = Query(None),
context: RequestContext = Depends(getRequestContext)
) -> PaginatedResponse[TrusteePosition]:
"""Get all positions with optional pagination."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
@ -1057,14 +1057,14 @@ async def get_positions(
@router.get("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
@limiter.limit("30/minute")
async def get_position(
def get_position(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition:
"""Get a single position by ID."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
position = interface.getPosition(positionId)
@ -1075,14 +1075,14 @@ async def get_position(
@router.get("/{instanceId}/positions/contract/{contractId}", response_model=List[TrusteePosition])
@limiter.limit("30/minute")
async def get_positions_by_contract(
def get_positions_by_contract(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
contractId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePosition]:
"""Get all positions for a contract."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getPositionsByContract(contractId)
@ -1090,14 +1090,14 @@ async def get_positions_by_contract(
@router.get("/{instanceId}/positions/organisation/{orgId}", response_model=List[TrusteePosition])
@limiter.limit("30/minute")
async def get_positions_by_organisation(
def get_positions_by_organisation(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
orgId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePosition]:
"""Get all positions for an organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return interface.getPositionsByOrganisation(orgId)
@ -1105,14 +1105,14 @@ async def get_positions_by_organisation(
@router.post("/{instanceId}/positions", response_model=TrusteePosition, status_code=201)
@limiter.limit("10/minute")
async def create_position(
def create_position(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteePosition = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition:
"""Create a new position."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createPosition(data.model_dump())
@ -1123,7 +1123,7 @@ async def create_position(
@router.put("/{instanceId}/positions/{positionId}", response_model=TrusteePosition)
@limiter.limit("10/minute")
async def update_position(
def update_position(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...),
@ -1131,7 +1131,7 @@ async def update_position(
context: RequestContext = Depends(getRequestContext)
) -> TrusteePosition:
"""Update a position."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getPosition(positionId)
@ -1146,14 +1146,14 @@ async def update_position(
@router.delete("/{instanceId}/positions/{positionId}")
@limiter.limit("10/minute")
async def delete_position(
def delete_position(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a position."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getPosition(positionId)
@ -1170,7 +1170,7 @@ async def delete_position(
@router.get("/{instanceId}/position-documents")
@limiter.limit("30/minute")
async def get_position_documents(
def get_position_documents(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
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.
"""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
paginationParams = _parsePagination(pagination)
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)
@limiter.limit("30/minute")
async def get_position_document(
def get_position_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
linkId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument:
"""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)
link = interface.getPositionDocument(linkId)
@ -1221,14 +1221,14 @@ async def get_position_document(
@router.get("/{instanceId}/position-documents/position/{positionId}", response_model=List[TrusteePositionDocument])
@limiter.limit("30/minute")
async def get_documents_for_position(
def get_documents_for_position(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
positionId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePositionDocument]:
"""Get all document links for a position."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
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])
@limiter.limit("30/minute")
async def get_positions_for_document(
def get_positions_for_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
documentId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> List[TrusteePositionDocument]:
"""Get all position links for a document."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
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)
@limiter.limit("10/minute")
async def create_position_document(
def create_position_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
data: TrusteePositionDocument = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument:
"""Create a new position-document link."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createPositionDocument(data.model_dump())
@ -1269,7 +1269,7 @@ async def create_position_document(
@router.put("/{instanceId}/position-documents/{linkId}", response_model=TrusteePositionDocument)
@limiter.limit("10/minute")
async def update_position_document(
def update_position_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
linkId: str = Path(...),
@ -1277,7 +1277,7 @@ async def update_position_document(
context: RequestContext = Depends(getRequestContext)
) -> TrusteePositionDocument:
"""Update a position-document link."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
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}")
@limiter.limit("10/minute")
async def delete_position_document(
def delete_position_document(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
linkId: str = Path(...),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Delete a position-document link."""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
existing = interface.getPositionDocument(linkId)
@ -1314,14 +1314,14 @@ async def delete_position_document(
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.
Returns the mandateId if authorized.
This checks for the RESOURCE permission 'instance-roles.manage'.
"""
mandateId = await _validateInstanceAccess(instanceId, context)
mandateId = _validateInstanceAccess(instanceId, context)
# SysAdmin always has access
if context.user.isSysAdmin:
@ -1350,7 +1350,7 @@ async def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> st
@router.get("/{instanceId}/instance-roles", response_model=PaginatedResponse)
@limiter.limit("30/minute")
async def get_instance_roles(
def get_instance_roles(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
context: RequestContext = Depends(getRequestContext)
@ -1359,7 +1359,7 @@ async def get_instance_roles(
Get all roles for this feature instance.
Requires feature admin permission.
"""
mandateId = await _validateInstanceAdmin(instanceId, context)
mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface()
@ -1374,14 +1374,14 @@ async def get_instance_roles(
@router.get("/{instanceId}/instance-roles/{roleId}", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def get_instance_role(
def get_instance_role(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"),
context: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""Get a specific instance role."""
mandateId = await _validateInstanceAdmin(instanceId, context)
mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface()
role = rootInterface.getRole(roleId)
@ -1398,7 +1398,7 @@ async def get_instance_role(
@router.get("/{instanceId}/instance-roles/{roleId}/rules", response_model=PaginatedResponse)
@limiter.limit("30/minute")
async def get_instance_role_rules(
def get_instance_role_rules(
request: Request,
instanceId: str = Path(..., description="Feature Instance 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.
Requires feature admin permission.
"""
mandateId = await _validateInstanceAdmin(instanceId, context)
mandateId = _validateInstanceAdmin(instanceId, context)
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)
@limiter.limit("10/minute")
async def create_instance_role_rule(
def create_instance_role_rule(
request: Request,
instanceId: str = Path(..., description="Feature Instance 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.
Requires feature admin permission.
"""
mandateId = await _validateInstanceAdmin(instanceId, context)
mandateId = _validateInstanceAdmin(instanceId, context)
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])
@limiter.limit("10/minute")
async def update_instance_role_rule(
def update_instance_role_rule(
request: Request,
instanceId: str = Path(..., description="Feature Instance 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.
Requires feature admin permission.
"""
mandateId = await _validateInstanceAdmin(instanceId, context)
mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface()
@ -1530,7 +1530,7 @@ async def update_instance_role_rule(
@router.delete("/{instanceId}/instance-roles/{roleId}/rules/{ruleId}")
@limiter.limit("10/minute")
async def delete_instance_role_rule(
def delete_instance_role_rule(
request: Request,
instanceId: str = Path(..., description="Feature Instance ID"),
roleId: str = Path(..., description="Role ID"),
@ -1541,7 +1541,7 @@ async def delete_instance_role_rule(
Delete an AccessRule for an instance role.
Requires feature admin permission.
"""
mandateId = await _validateInstanceAdmin(instanceId, context)
mandateId = _validateInstanceAdmin(instanceId, context)
rootInterface = getRootInterface()

View file

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

View file

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

View file

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

View file

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

View file

@ -68,7 +68,7 @@ router = APIRouter(
@router.get("/", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute")
async def list_roles(
def list_roles(
request: Request,
currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]:
@ -113,7 +113,7 @@ async def list_roles(
@router.get("/options", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute")
async def get_role_options(
def get_role_options(
request: Request,
currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]:
@ -154,7 +154,7 @@ async def get_role_options(
@router.post("/", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def create_role(
def create_role(
request: Request,
role: Role = Body(...),
currentUser: User = Depends(requireSysAdmin)
@ -198,7 +198,7 @@ async def create_role(
@router.get("/{roleId}", response_model=Dict[str, Any])
@limiter.limit("60/minute")
async def get_role(
def get_role(
request: Request,
roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin)
@ -242,7 +242,7 @@ async def get_role(
@router.put("/{roleId}", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def update_role(
def update_role(
request: Request,
roleId: str = Path(..., description="Role ID"),
role: Role = Body(...),
@ -290,7 +290,7 @@ async def update_role(
@router.delete("/{roleId}", response_model=Dict[str, str])
@limiter.limit("30/minute")
async def delete_role(
def delete_role(
request: Request,
roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin)
@ -334,7 +334,7 @@ async def delete_role(
@router.get("/users", response_model=List[Dict[str, Any]])
@limiter.limit("60/minute")
async def list_users_with_roles(
def list_users_with_roles(
request: Request,
roleLabel: Optional[str] = Query(None, description="Filter by role label"),
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])
@limiter.limit("60/minute")
async def get_user_roles(
def get_user_roles(
request: Request,
userId: str = Path(..., description="User ID"),
currentUser: User = Depends(requireSysAdmin)
@ -446,7 +446,7 @@ async def get_user_roles(
@router.put("/users/{userId}/roles", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def update_user_roles(
def update_user_roles(
request: Request,
userId: str = Path(..., description="User ID"),
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])
@limiter.limit("30/minute")
async def add_user_role(
def add_user_role(
request: Request,
userId: str = Path(..., description="User ID"),
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])
@limiter.limit("30/minute")
async def remove_user_role(
def remove_user_role(
request: Request,
userId: str = Path(..., description="User ID"),
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]])
@limiter.limit("60/minute")
async def get_users_with_role(
def get_users_with_role(
request: Request,
roleLabel: str = Path(..., description="Role label"),
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)
@limiter.limit("300/minute") # Raised from 60 - sidebar checks many pages individually
async def get_permissions(
def get_permissions(
request: Request,
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)"),
@ -101,7 +101,7 @@ async def get_permissions(
@router.get("/permissions/all", response_model=Dict[str, Any])
@limiter.limit("120/minute") # Raised from 30 - optimized endpoint for bulk permission fetch
async def get_all_permissions(
def get_all_permissions(
request: Request,
context: Optional[str] = Query(None, description="Context type: UI or RESOURCE (if not provided, returns both)"),
reqContext: RequestContext = Depends(getRequestContext)
@ -293,7 +293,7 @@ async def get_all_permissions(
@router.get("/rules", response_model=PaginatedResponse)
@limiter.limit("30/minute")
async def get_access_rules(
def get_access_rules(
request: Request,
roleLabel: Optional[str] = Query(None, description="Filter by role label"),
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)
@limiter.limit("30/minute")
async def get_access_rules_by_role(
def get_access_rules_by_role(
request: Request,
roleId: str = Path(..., description="Role ID to get rules for"),
currentUser: User = Depends(requireSysAdmin)
@ -420,7 +420,7 @@ async def get_access_rules_by_role(
@router.get("/rules/{ruleId}", response_model=dict)
@limiter.limit("30/minute")
async def get_access_rule(
def get_access_rule(
request: Request,
ruleId: str = Path(..., description="Access rule ID"),
currentUser: User = Depends(requireSysAdmin)
@ -462,7 +462,7 @@ async def get_access_rule(
@router.post("/rules", response_model=dict)
@limiter.limit("30/minute")
async def create_access_rule(
def create_access_rule(
request: Request,
accessRuleData: dict = Body(..., description="Access rule data"),
currentUser: User = Depends(requireSysAdmin)
@ -528,7 +528,7 @@ async def create_access_rule(
@router.put("/rules/{ruleId}", response_model=dict)
@limiter.limit("30/minute")
async def update_access_rule(
def update_access_rule(
request: Request,
ruleId: str = Path(..., description="Access rule ID"),
accessRuleData: dict = Body(..., description="Updated access rule data"),
@ -611,7 +611,7 @@ async def update_access_rule(
@router.delete("/rules/{ruleId}")
@limiter.limit("30/minute")
async def delete_access_rule(
def delete_access_rule(
request: Request,
ruleId: str = Path(..., description="Access rule ID"),
currentUser: User = Depends(requireSysAdmin)
@ -669,7 +669,7 @@ async def delete_access_rule(
@router.get("/roles", response_model=PaginatedResponse)
@limiter.limit("60/minute")
async def list_roles(
def list_roles(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
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]])
@limiter.limit("60/minute")
async def get_role_options(
def get_role_options(
request: Request,
currentUser: User = Depends(requireSysAdmin)
) -> List[Dict[str, Any]]:
@ -879,7 +879,7 @@ async def get_role_options(
@router.post("/roles", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def create_role(
def create_role(
request: Request,
role: Role = Body(...),
currentUser: User = Depends(requireSysAdmin)
@ -928,7 +928,7 @@ async def create_role(
@router.get("/roles/{roleId}", response_model=Dict[str, Any])
@limiter.limit("60/minute")
async def get_role(
def get_role(
request: Request,
roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin)
@ -975,7 +975,7 @@ async def get_role(
@router.put("/roles/{roleId}", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def update_role(
def update_role(
request: Request,
roleId: str = Path(..., description="Role ID"),
role: Role = Body(...),
@ -1028,7 +1028,7 @@ async def update_role(
@router.delete("/roles/{roleId}", response_model=Dict[str, str])
@limiter.limit("30/minute")
async def delete_role(
def delete_role(
request: Request,
roleId: str = Path(..., description="Role ID"),
currentUser: User = Depends(requireSysAdmin)
@ -1078,7 +1078,7 @@ async def delete_role(
@router.get("/catalog/objects", response_model=Dict[str, Any])
@limiter.limit("60/minute")
async def getCatalogObjects(
def getCatalogObjects(
request: Request,
context: Optional[str] = Query(None, description="Filter by context (DATA, UI, RESOURCE)"),
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])
@limiter.limit("60/minute")
async def getCatalogStats(
def getCatalogStats(
request: Request,
currentUser: User = Depends(requireSysAdmin)
) -> Dict[str, Any]:
@ -1200,7 +1200,7 @@ async def getCatalogStats(
@router.post("/cleanup/duplicate-rules", response_model=dict)
@limiter.limit("5/minute")
async def cleanup_duplicate_access_rules(
def cleanup_duplicate_access_rules(
request: Request,
dryRun: bool = Query(True, description="If true, only report duplicates without deleting"),
currentUser: User = Depends(requireSysAdmin)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -50,7 +50,7 @@ def getServiceChat(currentUser: User):
# Consolidated endpoint for getting all workflows
@router.get("/", response_model=PaginatedResponse[ChatWorkflow])
@limiter.limit("120/minute")
async def get_workflows(
def get_workflows(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser)
@ -123,7 +123,7 @@ async def get_workflows(
@router.get("/{workflowId}", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def get_workflow(
def get_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
currentUser: User = Depends(getCurrentUser)
@ -152,7 +152,7 @@ async def get_workflow(
@router.put("/{workflowId}", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def update_workflow(
def update_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow to update"),
workflowData: Dict[str, Any] = Body(...),
@ -200,7 +200,7 @@ async def update_workflow(
# API Endpoint for workflow status
@router.get("/{workflowId}/status", response_model=ChatWorkflow)
@limiter.limit("120/minute")
async def get_workflow_status(
def get_workflow_status(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
currentUser: User = Depends(getCurrentUser)
@ -274,7 +274,7 @@ async def stop_workflow(
# API Endpoint for workflow logs with selective data transfer
@router.get("/{workflowId}/logs", response_model=PaginatedResponse[ChatLog])
@limiter.limit("120/minute")
async def get_workflow_logs(
def get_workflow_logs(
request: Request,
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)"),
@ -365,7 +365,7 @@ async def get_workflow_logs(
# API Endpoint for workflow messages with selective data transfer
@router.get("/{workflowId}/messages", response_model=PaginatedResponse[ChatMessage])
@limiter.limit("120/minute")
async def get_workflow_messages(
def get_workflow_messages(
request: Request,
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)"),
@ -457,7 +457,7 @@ async def get_workflow_messages(
# State 11: Workflow Reset/Deletion endpoint
@router.delete("/{workflowId}", response_model=Dict[str, Any])
@limiter.limit("120/minute")
async def delete_workflow(
def delete_workflow(
request: Request,
workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: User = Depends(getCurrentUser)
@ -516,7 +516,7 @@ async def delete_workflow(
@router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any])
@limiter.limit("120/minute")
async def delete_workflow_message(
def delete_workflow_message(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
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])
@limiter.limit("120/minute")
async def delete_file_from_message(
def delete_file_from_message(
request: Request,
workflowId: str = Path(..., description="ID of the workflow"),
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])
@limiter.limit("120/minute")
async def get_all_actions(
def get_all_actions(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
@ -685,7 +685,7 @@ async def get_all_actions(
@router.get("/actions/{method}", response_model=Dict[str, Any])
@limiter.limit("120/minute")
async def get_method_actions(
def get_method_actions(
request: Request,
method: str = Path(..., description="Method name (e.g., 'outlook', 'sharepoint')"),
currentUser: User = Depends(getCurrentUser)
@ -768,7 +768,7 @@ async def get_method_actions(
@router.get("/actions/{method}/{action}", response_model=Dict[str, Any])
@limiter.limit("120/minute")
async def get_action_schema(
def get_action_schema(
request: Request,
method: str = Path(..., description="Method name (e.g., 'outlook', 'sharepoint')"),
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)
@limiter.limit("5/minute")
async def export_user_data(
def export_user_data(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> DataExportResponse:
@ -215,7 +215,7 @@ async def export_user_data(
@router.get("/data-portability")
@limiter.limit("5/minute")
async def export_portable_data(
def export_portable_data(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> JSONResponse:
@ -296,7 +296,7 @@ async def export_portable_data(
@router.delete("/", response_model=DeletionResult)
@limiter.limit("1/hour")
async def delete_account(
def delete_account(
request: Request,
confirmDeletion: bool = False,
currentUser: User = Depends(getCurrentUser)
@ -391,7 +391,7 @@ async def delete_account(
@router.get("/consent-info", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def get_consent_info(
def get_consent_info(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:

View file

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

View file

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

View file

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

View file

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

View file

@ -93,7 +93,7 @@ SCOPES = [
]
@router.get("/config")
async def get_config():
def get_config():
"""Debug endpoint to check Google OAuth configuration"""
return {
"client_id": CLIENT_ID,
@ -109,7 +109,7 @@ async def get_config():
@router.get("/login")
@limiter.limit("5/minute")
async def login(
def login(
request: Request,
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")
@ -589,7 +589,7 @@ async def auth_callback(code: str, state: str, request: Request, response: Respo
@router.get("/me", response_model=User)
@limiter.limit("30/minute")
async def get_current_user(
def get_current_user(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> User:
@ -605,7 +605,7 @@ async def get_current_user(
@router.post("/logout")
@limiter.limit("10/minute")
async def logout(
def logout(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:

View file

@ -89,7 +89,7 @@ router = APIRouter(
@router.post("/login")
@limiter.limit("30/minute")
async def login(
def login(
request: Request,
response: Response,
formData: OAuth2PasswordRequestForm = Depends(),
@ -242,7 +242,7 @@ async def login(
@router.post("/register")
@limiter.limit("10/minute")
async def register_user(
def register_user(
request: Request,
userData: User = Body(...),
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)
@limiter.limit("30/minute")
async def read_user_me(
def read_user_me(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> User:
@ -397,7 +397,7 @@ async def read_user_me(
@router.post("/refresh")
@limiter.limit("60/minute")
async def refresh_token(
def refresh_token(
request: Request,
response: Response
) -> Dict[str, Any]:
@ -472,7 +472,7 @@ async def refresh_token(
@router.post("/logout")
@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"""
try:
# Get user interface with current user context
@ -541,7 +541,7 @@ async def logout(request: Request, response: Response, currentUser: User = Depen
@router.get("/available")
@limiter.limit("10/minute")
async def check_username_availability(
def check_username_availability(
request: Request,
username: str,
authenticationAuthority: str = "local"
@ -573,7 +573,7 @@ async def check_username_availability(
@router.post("/password-reset-request")
@limiter.limit("5/minute")
async def password_reset_request(
def password_reset_request(
request: Request,
username: 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")
@limiter.limit("10/minute")
async def password_reset(
def password_reset(
request: Request,
token: str = Body(..., embed=True),
password: str = Body(..., embed=True)

View file

@ -66,7 +66,7 @@ SCOPES = [
@router.get("/login")
@limiter.limit("5/minute")
async def login(
def login(
request: Request,
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")
@ -138,7 +138,7 @@ async def login(
@router.get("/adminconsent")
@limiter.limit("5/minute")
async def adminconsent(request: Request) -> RedirectResponse:
def adminconsent(request: Request) -> RedirectResponse:
"""Initiate Microsoft Admin Consent flow.
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")
async def adminconsent_callback(
def adminconsent_callback(
admin_consent: Optional[str] = Query(None),
tenant: 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)
@limiter.limit("30/minute")
async def get_current_user(
def get_current_user(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> User:
@ -619,7 +619,7 @@ async def get_current_user(
@router.post("/logout")
@limiter.limit("10/minute")
async def logout(
def logout(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
@ -655,7 +655,7 @@ async def logout(
@router.post("/cleanup")
@limiter.limit("5/minute")
async def cleanup_expired_tokens(
def cleanup_expired_tokens(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:

View file

@ -409,7 +409,7 @@ def _formatBlockItem(item: Dict[str, Any], language: str) -> Dict[str, Any]:
@navigationRouter.get("/navigation")
@limiter.limit("60/minute")
async def get_navigation(
def get_navigation(
request: Request,
language: str = Query("de", description="Language for labels (en, de, fr)"),
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})
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.append(executionLog)
# Keep only last 50 executions
if len(executionLogs) > 50:
executionLogs = executionLogs[-50:]
services.interfaceDbAutomation.updateAutomationDefinition(
automationId,
{"executionLogs": executionLogs}
)
services.interfaceDbAutomation._saveExecutionLog(automationId, executionLogs)
return workflow
except Exception as e:
@ -195,7 +192,7 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
executionLog["status"] = "error"
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:
automation = services.interfaceDbAutomation.getAutomationDefinition(automationId)
if automation:
@ -203,10 +200,7 @@ async def executeAutomation(automationId: str, services) -> ChatWorkflow:
executionLogs.append(executionLog)
if len(executionLogs) > 50:
executionLogs = executionLogs[-50:]
services.interfaceDbAutomation.updateAutomationDefinition(
automationId,
{"executionLogs": executionLogs}
)
services.interfaceDbAutomation._saveExecutionLog(automationId, executionLogs)
except Exception as 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())