gateway/modules/routes/routeMessaging.py
2026-02-08 14:26:01 +01:00

504 lines
18 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
from fastapi import APIRouter, HTTPException, Depends, Body, Path, Request, Query
from typing import List, Dict, Any, Optional
from fastapi import status
import logging
import json
# Import auth module
from modules.auth import limiter, getCurrentUser, getRequestContext, RequestContext
from modules.datamodels.datamodelRbac import Role
# Import interfaces
import modules.interfaces.interfaceDbManagement as interfaceDbManagement
from modules.datamodels.datamodelMessaging import (
MessagingSubscription,
MessagingSubscriptionRegistration,
MessagingDelivery,
MessagingChannel,
MessagingEventParameters,
MessagingSubscriptionExecutionResult
)
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
# Configure logger
logger = logging.getLogger(__name__)
# Create router for messaging endpoints
router = APIRouter(
prefix="/api/messaging",
tags=["Messaging"],
responses={404: {"description": "Not found"}}
)
# Subscription Endpoints
@router.get("/subscriptions", response_model=PaginatedResponse[MessagingSubscription])
@limiter.limit("60/minute")
def get_subscriptions(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser)
) -> PaginatedResponse[MessagingSubscription]:
"""Get subscriptions with optional pagination, sorting, and filtering."""
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbManagement.getInterface(currentUser)
result = managementInterface.getAllSubscriptions(pagination=paginationParams)
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
@router.post("/subscriptions", response_model=MessagingSubscription)
@limiter.limit("60/minute")
def create_subscription(
request: Request,
subscription: MessagingSubscription,
currentUser: User = Depends(getCurrentUser)
) -> MessagingSubscription:
"""Create a new subscription"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
subscriptionData = subscription.model_dump(exclude={"id"})
newSubscription = managementInterface.createSubscription(subscriptionData)
return MessagingSubscription(**newSubscription)
@router.get("/subscriptions/{subscriptionId}", response_model=MessagingSubscription)
@limiter.limit("60/minute")
def get_subscription(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"),
currentUser: User = Depends(getCurrentUser)
) -> MessagingSubscription:
"""Get a specific subscription"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
subscription = managementInterface.getSubscription(subscriptionId)
if not subscription:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Subscription with ID {subscriptionId} not found"
)
return subscription
@router.put("/subscriptions/{subscriptionId}", response_model=MessagingSubscription)
@limiter.limit("60/minute")
def update_subscription(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription to update"),
subscriptionData: MessagingSubscription = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> MessagingSubscription:
"""Update an existing subscription"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
existingSubscription = managementInterface.getSubscription(subscriptionId)
if not existingSubscription:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Subscription with ID {subscriptionId} not found"
)
updateData = subscriptionData.model_dump(exclude={"id", "subscriptionId"})
updatedSubscription = managementInterface.updateSubscription(subscriptionId, updateData)
if not updatedSubscription:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error updating the subscription"
)
return MessagingSubscription(**updatedSubscription)
@router.delete("/subscriptions/{subscriptionId}", response_model=Dict[str, Any])
@limiter.limit("60/minute")
def delete_subscription(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription to delete"),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Delete a subscription"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
existingSubscription = managementInterface.getSubscription(subscriptionId)
if not existingSubscription:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Subscription with ID {subscriptionId} not found"
)
success = managementInterface.deleteSubscription(subscriptionId)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error deleting the subscription"
)
return {"message": f"Subscription with ID {subscriptionId} successfully deleted"}
# Registration Endpoints
@router.get("/subscriptions/{subscriptionId}/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration])
@limiter.limit("60/minute")
def get_subscription_registrations(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"),
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser)
) -> PaginatedResponse[MessagingSubscriptionRegistration]:
"""Get registrations for a subscription"""
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbManagement.getInterface(currentUser)
result = managementInterface.getAllRegistrations(
subscriptionId=subscriptionId,
pagination=paginationParams
)
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
@router.post("/subscriptions/{subscriptionId}/subscribe", response_model=MessagingSubscriptionRegistration)
@limiter.limit("60/minute")
def subscribe_user(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"),
channel: MessagingChannel = Body(..., embed=True),
channelConfig: str = Body(..., embed=True),
currentUser: User = Depends(getCurrentUser)
) -> MessagingSubscriptionRegistration:
"""Subscribe user to a subscription with a specific channel"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
registration = managementInterface.subscribeUser(
subscriptionId=subscriptionId,
userId=currentUser.id,
channel=channel,
channelConfig=channelConfig
)
return MessagingSubscriptionRegistration(**registration)
@router.delete("/subscriptions/{subscriptionId}/unsubscribe", response_model=Dict[str, Any])
@limiter.limit("60/minute")
def unsubscribe_user(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription"),
channel: MessagingChannel = Body(..., embed=True),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Unsubscribe user from a subscription for a specific channel"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
success = managementInterface.unsubscribeUser(
subscriptionId=subscriptionId,
userId=currentUser.id,
channel=channel
)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Registration not found"
)
return {"message": f"Successfully unsubscribed from {subscriptionId} for channel {channel.value}"}
@router.get("/registrations", response_model=PaginatedResponse[MessagingSubscriptionRegistration])
@limiter.limit("60/minute")
def get_my_registrations(
request: Request,
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
currentUser: User = Depends(getCurrentUser)
) -> PaginatedResponse[MessagingSubscriptionRegistration]:
"""Get own registrations"""
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbManagement.getInterface(currentUser)
result = managementInterface.getAllRegistrations(
userId=currentUser.id,
pagination=paginationParams
)
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
@router.put("/registrations/{registrationId}", response_model=MessagingSubscriptionRegistration)
@limiter.limit("60/minute")
def update_registration(
request: Request,
registrationId: str = Path(..., description="ID of the registration to update"),
registrationData: MessagingSubscriptionRegistration = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> MessagingSubscriptionRegistration:
"""Update a registration"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
existingRegistration = managementInterface.getRegistration(registrationId)
if not existingRegistration:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Registration with ID {registrationId} not found"
)
updateData = registrationData.model_dump(exclude={"id", "subscriptionId", "userId"})
updatedRegistration = managementInterface.updateRegistration(registrationId, updateData)
if not updatedRegistration:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error updating the registration"
)
return MessagingSubscriptionRegistration(**updatedRegistration)
@router.delete("/registrations/{registrationId}", response_model=Dict[str, Any])
@limiter.limit("60/minute")
def delete_registration(
request: Request,
registrationId: str = Path(..., description="ID of the registration to delete"),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Delete a registration"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
existingRegistration = managementInterface.getRegistration(registrationId)
if not existingRegistration:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Registration with ID {registrationId} not found"
)
success = managementInterface.deleteRegistration(registrationId)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error deleting the registration"
)
return {"message": f"Registration with ID {registrationId} successfully deleted"}
# Trigger Endpoints
def _getTriggerKey(request: Request) -> str:
"""Custom key function for trigger rate limiting per subscriptionId"""
subscriptionId = request.path_params.get("subscriptionId", "unknown")
return f"{request.client.host}:{subscriptionId}"
@router.post("/trigger/{subscriptionId}", response_model=MessagingSubscriptionExecutionResult)
@limiter.limit("60/minute", key_func=_getTriggerKey)
def trigger_subscription(
request: Request,
subscriptionId: str = Path(..., description="ID of the subscription to trigger"),
eventParameters: Dict[str, Any] = Body(...),
context: RequestContext = Depends(getRequestContext)
) -> MessagingSubscriptionExecutionResult:
"""
Trigger a subscription with event parameters.
Requires Mandate-Admin role or SysAdmin.
"""
# RBAC-Check: Admin or Mandate-Admin can trigger
if not _hasTriggerPermission(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin or Mandate-Admin role required to trigger subscriptions"
)
# Get messaging service from request app state
from modules.services import getInterface as getServicesInterface
services = getServicesInterface(context.user, None, mandateId=str(context.mandateId))
# Konvertiere Dict zu Pydantic Model
eventParams = MessagingEventParameters(triggerData=eventParameters)
executionResult = services.messaging.executeSubscription(subscriptionId, eventParams)
return executionResult
def _hasTriggerPermission(context: RequestContext) -> bool:
"""
Check if user has permission to trigger subscriptions.
Requires admin or mandate-admin role.
"""
if context.isSysAdmin:
return True
if not context.roleIds:
return False
try:
from modules.interfaces.interfaceDbApp import getRootInterface
rootInterface = getRootInterface()
for roleId in context.roleIds:
role = rootInterface.getRole(roleId)
if role:
roleLabel = role.roleLabel
# Admin role at mandate level or system admin
if roleLabel in ("admin", "sysadmin"):
return True
return False
except Exception as e:
logger.error(f"Error checking trigger permission: {e}")
return False
# Delivery Endpoints
@router.get("/deliveries", response_model=PaginatedResponse[MessagingDelivery])
@limiter.limit("60/minute")
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"),
currentUser: User = Depends(getCurrentUser)
) -> PaginatedResponse[MessagingDelivery]:
"""Get delivery history"""
paginationParams = None
if pagination:
try:
paginationDict = json.loads(pagination)
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(
status_code=400,
detail=f"Invalid pagination parameter: {str(e)}"
)
managementInterface = interfaceDbManagement.getInterface(currentUser)
result = managementInterface.getDeliveries(
subscriptionId=subscriptionId,
userId=currentUser.id, # Users can only see their own deliveries
pagination=paginationParams
)
if paginationParams:
return PaginatedResponse(
items=result.items,
pagination=PaginationMetadata(
currentPage=paginationParams.page,
pageSize=paginationParams.pageSize,
totalItems=result.totalItems,
totalPages=result.totalPages,
sort=paginationParams.sort,
filters=paginationParams.filters
)
)
else:
return PaginatedResponse(
items=result,
pagination=None
)
@router.get("/deliveries/{deliveryId}", response_model=MessagingDelivery)
@limiter.limit("60/minute")
def get_delivery(
request: Request,
deliveryId: str = Path(..., description="ID of the delivery"),
currentUser: User = Depends(getCurrentUser)
) -> MessagingDelivery:
"""Get a specific delivery"""
managementInterface = interfaceDbManagement.getInterface(currentUser)
delivery = managementInterface.getDelivery(deliveryId)
if not delivery:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Delivery with ID {deliveryId} not found"
)
return delivery