# 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 # Import interfaces import modules.interfaces.interfaceDbComponentObjects as interfaceDbComponentObjects 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") async def getSubscriptions( 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 = interfaceDbComponentObjects.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") async def createSubscription( request: Request, subscription: MessagingSubscription, currentUser: User = Depends(getCurrentUser) ) -> MessagingSubscription: """Create a new subscription""" managementInterface = interfaceDbComponentObjects.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") async def getSubscription( request: Request, subscriptionId: str = Path(..., description="ID of the subscription"), currentUser: User = Depends(getCurrentUser) ) -> MessagingSubscription: """Get a specific subscription""" managementInterface = interfaceDbComponentObjects.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") async def updateSubscription( 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 = interfaceDbComponentObjects.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") async def deleteSubscription( request: Request, subscriptionId: str = Path(..., description="ID of the subscription to delete"), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a subscription""" managementInterface = interfaceDbComponentObjects.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") async def getSubscriptionRegistrations( 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 = interfaceDbComponentObjects.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") async def subscribeUser( 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 = interfaceDbComponentObjects.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") async def unsubscribeUser( 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 = interfaceDbComponentObjects.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") async def getMyRegistrations( 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 = interfaceDbComponentObjects.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") async def updateRegistration( request: Request, registrationId: str = Path(..., description="ID of the registration to update"), registrationData: MessagingSubscriptionRegistration = Body(...), currentUser: User = Depends(getCurrentUser) ) -> MessagingSubscriptionRegistration: """Update a registration""" managementInterface = interfaceDbComponentObjects.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") async def deleteRegistration( request: Request, registrationId: str = Path(..., description="ID of the registration to delete"), currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Delete a registration""" managementInterface = interfaceDbComponentObjects.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) async def triggerSubscription( request: Request, subscriptionId: str = Path(..., description="ID of the subscription to trigger"), eventParameters: Dict[str, Any] = Body(...), currentUser: User = Depends(getCurrentUser) ) -> MessagingSubscriptionExecutionResult: """Trigger a subscription with event parameters""" # RBAC-Check: Nur Admin/Mandate-Admin kann triggern # TODO: Add proper RBAC check here # Get messaging service from request app state # We need to access services through the request from modules.services import getInterface as getServicesInterface services = getServicesInterface(currentUser, None) # Konvertiere Dict zu Pydantic Model eventParams = MessagingEventParameters(triggerData=eventParameters) executionResult = services.messaging.executeSubscription(subscriptionId, eventParams) return executionResult # Delivery Endpoints @router.get("/deliveries", response_model=PaginatedResponse[MessagingDelivery]) @limiter.limit("60/minute") async def getDeliveries( 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 = interfaceDbComponentObjects.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") async def getDelivery( request: Request, deliveryId: str = Path(..., description="ID of the delivery"), currentUser: User = Depends(getCurrentUser) ) -> MessagingDelivery: """Get a specific delivery""" managementInterface = interfaceDbComponentObjects.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