from fastapi import APIRouter, HTTPException, Depends, Query from fastapi.responses import JSONResponse from backend.models.models import MessageTemplateDoc from backend.application.template_message_hub import TemplateMessageHub from common.token.token_manager import TokenManager from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel from typing import Dict, List import json from datetime import datetime router = APIRouter() template_message_hub = TemplateMessageHub() token_manager = TokenManager() security = HTTPBearer() # Define the request body schema class TemplateCreateRequest(BaseModel): template_id: str tenant_id: str = None region: int subject: str body: str is_active: bool = True class TemplateUpdateRequest(BaseModel): subject: str = None body: str = None is_active: bool = None region: int = None class TemplateAssignRequest(BaseModel): template_ids: List[str] region: int class TemplateRenderRequest(BaseModel): properties: Dict class TemplateCopyRequest(BaseModel): template_id: str new_template_id: str region: int # check credentials for admin and tenant def admin_only(credentials: HTTPAuthorizationCredentials = Depends(security)): try: payload = token_manager.decode_token(credentials.credentials) # handle token generated by structure Authentication service if "subject" in payload: # Authentication format: payload.subject.role role = payload.get("subject", {}).get("role") else: # local generated format: payload.role role = payload.get("role") # according to AdministrativeRole enum defined in authentication service, ADMINISTRATOR = 8 if role not in ["admin", 8]: raise HTTPException(status_code=403, detail="Admin access required") return payload except Exception: raise HTTPException(status_code=401, detail="Invalid token") def tenant_only(credentials: HTTPAuthorizationCredentials = Depends(security)): try: payload = token_manager.decode_token(credentials.credentials) # handle token generated by structure Authentication service if "subject" in payload: tenant_id = payload.get("subject", {}).get("id") user_id = payload.get("subject", {}).get("id") role = payload.get("subject", {}).get("role") else: tenant_id = payload.get("tenant_id") user_id = payload.get("id") role = payload.get("role") if not tenant_id: raise HTTPException(status_code=403, detail="Tenant access required") # according to AdministrativeRole enum defined in authentication service, BUSINESS = 2 if role not in [2, "tenant"]: raise HTTPException(status_code=403, detail="Tenant access required") payload["tenant_id"] = tenant_id payload["user_id"] = user_id payload["is_admin"] = False return payload except Exception: raise HTTPException(status_code=401, detail="Invalid token") # verify tenant access for template def verify_tenant_access(template_id: str, user_tenant_id: str, template_message_hub: TemplateMessageHub, allow_global_template: bool = True, region: int = None): async def _verify(): try: if not template_id: raise HTTPException(status_code=400, detail="Template ID is required") if user_tenant_id: if region is None: raise HTTPException(status_code=400, detail="Region parameter is required for tenant template access to ensure correct template version") template = await template_message_hub.verify_tenant_access(template_id, user_tenant_id, region) if not template: raise HTTPException(status_code=404, detail=f"Tenant template not found for template_id: {template_id}, tenant_id: {user_tenant_id}, region: {region}") if template.tenant_id != user_tenant_id: raise HTTPException(status_code=403, detail="Access denied: Template belongs to different tenant") return template else: raise HTTPException(status_code=403, detail="Tenant access required for template operations") except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail="Failed to verify template access") return _verify() # ==================== ADMIN API ==================== # Global template management API (Admin only) @router.post( "/admin/global_templates/create", dependencies=[Depends(admin_only)], operation_id="create_global_template", summary="Create a new global template (Admin Only)", description="Create a new global template that all tenants can use", response_description="Success/failure response in creating the global template" ) async def create_global_template(request: TemplateCreateRequest): try: result = await template_message_hub.create_global_template( template_id=request.template_id, region=request.region, subject=request.subject, body=request.body, is_active=request.is_active ) return JSONResponse( content={"message": "Global template created successfully", "template_id": request.template_id}, status_code=201 ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=f"Failed to create global template: {str(e)}") @router.put( "/admin/global_templates/update/{template_id}", dependencies=[Depends(admin_only)], operation_id="update_global_template", summary="Update a global template (Admin Only)", description="Update an existing global template", response_description="Success/failure response in updating the global template" ) async def update_global_template(template_id: str, request: TemplateUpdateRequest): try: region = request.region if region is None: raise HTTPException(status_code=400, detail="Region parameter is required for global template update to ensure correct template version") update_data = {k: v for k, v in request.dict().items() if v is not None and k != "region"} if not update_data: raise ValueError("At least one field must be provided for update") result = await template_message_hub.update_global_template(template_id, update_data, region) return JSONResponse( content={"message": "Global template updated successfully", "result": result}, status_code=200 ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail="Failed to update global template") @router.delete( "/admin/global_templates/delete/{template_id}", dependencies=[Depends(admin_only)], operation_id="delete_global_template", summary="Delete a global template (Admin Only)", description="Delete an existing global template", response_description="Success/failure response in deleting the global template" ) async def delete_global_template(template_id: str): try: result = await template_message_hub.delete_global_template(template_id) return JSONResponse( content={"message": "Global template deleted successfully", "result": result}, status_code=200 ) except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete global template") # ==================== SHARED API ==================== # Global template list API (Admin and Tenant) @router.get( "/global_templates/list", operation_id="list_global_templates", summary="List global templates (Admin and Tenant)", description="Get a list of all global templates (read-only operation)", response_description="List of global templates" ) async def list_global_templates(region: int): try: templates = await template_message_hub.list_global_templates(region) return JSONResponse( content={"templates": templates}, status_code=200 ) except Exception as e: raise HTTPException(status_code=500, detail="Failed to list global templates") # ==================== TENANT API ==================== # Tenant template management API (Tenant only) @router.get( "/tenant/templates/list", dependencies=[Depends(tenant_only)], operation_id="list_tenant_templates", summary="List tenant templates (Tenant Only)", description="Get a list of templates belonging to the current tenant", response_description="List of tenant templates" ) async def list_tenant_templates(region: int, payload: dict = Depends(tenant_only)): try: tenant_id = payload.get("tenant_id") templates = await template_message_hub.list_tenant_templates(tenant_id, region) return JSONResponse( content={"templates": templates}, status_code=200 ) except Exception as e: raise HTTPException(status_code=500, detail="Failed to list tenant templates") @router.post( "/tenant/templates/assign", dependencies=[Depends(tenant_only)], operation_id="assign_templates", summary="Assign global templates to tenant (Tenant Only)", description="Assign one or more global templates to the current tenant", response_description="Success/failure response in assigning templates" ) async def assign_templates_to_tenant(request: TemplateAssignRequest, payload: dict = Depends(tenant_only)): try: tenant_id = payload.get("tenant_id") results = await template_message_hub.assign_templates_to_tenant(tenant_id, request.template_ids, request.region) return JSONResponse( content={"message": "Templates assigned successfully", "results": results}, status_code=200 ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail="Failed to assign templates") @router.post( "/tenant/templates/create", dependencies=[Depends(tenant_only)], operation_id="create_tenant_template", summary="Create a tenant template (Tenant Only)", description="Create a new template for the current tenant", response_description="Success/failure response in creating the tenant template" ) async def create_tenant_template(request: TemplateCreateRequest, payload: dict = Depends(tenant_only)): try: tenant_id = payload.get("tenant_id") result = await template_message_hub.create_tenant_template( template_id=request.template_id, region=request.region, subject=request.subject, body=request.body, tenant_id=tenant_id, is_active=request.is_active ) if hasattr(result, 'dict'): result_dict = result.dict() for key, value in result_dict.items(): if hasattr(value, 'isoformat'): result_dict[key] = value.isoformat() elif hasattr(value, 'value'): result_dict[key] = value.value else: result_dict = { "template_id": getattr(result, 'template_id', request.template_id), "tenant_id": getattr(result, 'tenant_id', tenant_id), "region": getattr(result, 'region', request.region), "subject": getattr(result, 'subject', request.subject), "body": getattr(result, 'body', request.body), "is_active": getattr(result, 'is_active', request.is_active) } return JSONResponse( content={"message": "Tenant template created successfully", "result": result_dict}, status_code=201, media_type="application/json" ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: import traceback traceback.print_exc() raise HTTPException(status_code=500, detail="Failed to create tenant template") @router.put( "/tenant/templates/update/{template_id}", dependencies=[Depends(tenant_only)], operation_id="update_tenant_template", summary="Update a tenant template (Tenant Only)", description="Update an existing template for the current tenant", response_description="Success/failure response in updating the tenant template" ) async def update_tenant_template(template_id: str, request: TemplateUpdateRequest, payload: dict = Depends(tenant_only)): try: tenant_id = payload.get("tenant_id") region = request.region if region is None: raise HTTPException(status_code=400, detail="Region parameter is required for tenant template update to ensure correct template version") await verify_tenant_access(template_id, tenant_id, template_message_hub, allow_global_template=False, region=region) update_data = {k: v for k, v in request.dict().items() if v is not None and k != "region"} if not update_data: raise ValueError("At least one field must be provided for update") result = await template_message_hub.update_tenant_template(tenant_id, template_id, update_data, region) if isinstance(result, dict): result_dict = result else: result_dict = {"success": True, "template_id": template_id} return JSONResponse( content={"message": "Tenant template updated successfully", "result": result_dict}, status_code=200 ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except PermissionError: raise HTTPException(status_code=403, detail="Forbidden") except Exception as e: raise HTTPException(status_code=500, detail="Failed to update tenant template") @router.delete( "/tenant/templates/delete/{template_id}", dependencies=[Depends(tenant_only)], operation_id="delete_tenant_template", summary="Delete a tenant template (Tenant Only)", description="Delete an existing template for the current tenant", response_description="Success/failure response in deleting the tenant template" ) async def delete_tenant_template(template_id: str, region: int = Query(..., description="Template region"), payload: dict = Depends(tenant_only)): try: tenant_id = payload.get("tenant_id") await verify_tenant_access(template_id, tenant_id, template_message_hub, allow_global_template=False, region=region) result = await template_message_hub.delete_tenant_template(tenant_id, template_id, region) if isinstance(result, dict): result_dict = result else: result_dict = {"success": True, "template_id": template_id} return JSONResponse( content={"message": "Tenant template deleted successfully", "result": result_dict}, status_code=200 ) except PermissionError: raise HTTPException(status_code=403, detail="Forbidden") except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete tenant template") @router.post( "/tenant/templates/render/{template_id}", dependencies=[Depends(tenant_only)], operation_id="render_template", summary="Render a template (Tenant Only)", description="Render a template with provided properties (can render own templates or assigned global templates)", response_description="Rendered template content" ) async def render_template(template_id: str, request: TemplateRenderRequest, region: int = Query(..., description="Template region"), payload: dict = Depends(tenant_only)): try: tenant_id = payload.get("tenant_id") # Allow access to both tenant templates and assigned global templates await verify_tenant_access(template_id, tenant_id, template_message_hub, allow_global_template=True, region=region) result = await template_message_hub.render_template(tenant_id, template_id, request.properties, region) return JSONResponse( content={"message": "Template rendered successfully", "result": result}, status_code=200 ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail="Failed to render template")