From 6ce3e4accec8f53cf1a86a437849d63ca4d2f9bf Mon Sep 17 00:00:00 2001 From: deepvasoya Date: Thu, 12 Jun 2025 13:49:08 +0530 Subject: [PATCH] feat: new endpoints for agent feat: new auth middleware for agent --- apis/__init__.py | 7 +++- apis/endpoints/agent.py | 18 ++++++++++ apis/endpoints/auth.py | 3 +- apis/endpoints/clinicDoctor.py | 4 ++- middleware/auth_secret.py | 29 ++++++++++++++++ services/agentServices.py | 15 +++++++++ services/clinicDoctorsServices.py | 56 ++++++++++++++++++++++++------- services/clinicServices.py | 17 ++++++++++ 8 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 apis/endpoints/agent.py create mode 100644 middleware/auth_secret.py create mode 100644 services/agentServices.py diff --git a/apis/__init__.py b/apis/__init__.py index 65e728a..db9081c 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -1,11 +1,12 @@ from fastapi import APIRouter, Depends from middleware.auth_dependency import auth_required +from middleware.auth_secret import verify_secret from fastapi.security import HTTPBearer # Import the security scheme bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") -from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts, notifications,sns, stripe +from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts, notifications,sns, stripe, agent api_router = APIRouter() @@ -42,3 +43,7 @@ api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboar api_router.include_router(call_transcripts.router, prefix="/call-transcripts", tags=["call-transcripts"]) api_router.include_router(notifications.router, prefix="/notifications", tags=["notifications"], dependencies=[Depends(auth_required)]) + + +# agent (bot) routes +api_router.include_router(agent.router, prefix="/agent", tags=["agent"], dependencies=[Depends(verify_secret)], include_in_schema=False) \ No newline at end of file diff --git a/apis/endpoints/agent.py b/apis/endpoints/agent.py new file mode 100644 index 0000000..8455a7b --- /dev/null +++ b/apis/endpoints/agent.py @@ -0,0 +1,18 @@ +''' +this route is for agent (bot) +''' + +from fastapi import APIRouter + +from services.agentServices import AgentServices + +router = APIRouter() + +@router.get("/clinic/docs/{clinic_id}") +async def get_clinic_doctors_with_appointments(clinic_id: int): + return await AgentServices().get_clinic_doctors_with_appointments(clinic_id) + +@router.get("/clinic/{phone}") +async def get_clinic_by_phone(phone: str): + return await AgentServices().get_clinic_by_phone(phone) + diff --git a/apis/endpoints/auth.py b/apis/endpoints/auth.py index e1beddc..db8503e 100644 --- a/apis/endpoints/auth.py +++ b/apis/endpoints/auth.py @@ -62,6 +62,7 @@ async def verify_otp(data: AuthOTP): @router.get("/is-valid-domain") async def is_valid_domain(req:Request): - host = req.headers.get("host") + host = req.client.host + print(host) is_valid = await ClinicServices().is_valid_domain(host) return status.HTTP_200_OK if is_valid else status.HTTP_404_NOT_FOUND diff --git a/apis/endpoints/clinicDoctor.py b/apis/endpoints/clinicDoctor.py index d27a7cb..61a0423 100644 --- a/apis/endpoints/clinicDoctor.py +++ b/apis/endpoints/clinicDoctor.py @@ -20,7 +20,9 @@ async def get_clinic_doctors( if page < 1: page = 1 offset = (page - 1) * limit - clinic_doctors = await ClinicDoctorsServices().get_clinic_doctors(req.state.user, limit, offset, search, sort_by, sort_order) + user = req.state.user + clinic_id = user["created_clinics"][0]["id"] + clinic_doctors = await ClinicDoctorsServices().get_clinic_doctors(clinic_id, limit, offset, search, sort_by, sort_order) return ApiResponse(data=clinic_doctors, message="Clinic doctors retrieved successfully") @router.post("/") diff --git a/middleware/auth_secret.py b/middleware/auth_secret.py new file mode 100644 index 0000000..ae12061 --- /dev/null +++ b/middleware/auth_secret.py @@ -0,0 +1,29 @@ +""" +Authentication middleware and dependency for agent (bot) requests. +Validates the presence and correctness of the X-Agent-Secret header. +""" +import os +from fastapi import HTTPException, status, Header +from typing import Optional +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Get the secret key from environment variables +AGENT_SECRET_KEY = os.getenv("AGENT_SECRET_KEY") +if not AGENT_SECRET_KEY: + raise ValueError("AGENT_SECRET_KEY environment variable not set") + +async def verify_secret(x_agent_secret: Optional[str] = Header(None, alias="X-Agent-Secret")): + """ + Dependency function to verify the X-Agent-Secret header. + Can be used with Depends() in FastAPI route dependencies. + """ + if not x_agent_secret or x_agent_secret != AGENT_SECRET_KEY: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing X-Agent-Secret header", + headers={"WWW-Authenticate": "Bearer"}, + ) + return True \ No newline at end of file diff --git a/services/agentServices.py b/services/agentServices.py new file mode 100644 index 0000000..31c82a7 --- /dev/null +++ b/services/agentServices.py @@ -0,0 +1,15 @@ +from services.clinicServices import ClinicServices +from services.clinicDoctorsServices import ClinicDoctorsServices + + +class AgentServices: + def __init__(self): + self.clinicServices = ClinicServices() + self.clinicDoctorService = ClinicDoctorsServices() + + async def get_clinic_by_phone(self, phone: str): + return await self.clinicServices.get_clinic_by_phone(phone) + + + async def get_clinic_doctors_with_appointments(self, clinic_id: int): + return await self.clinicDoctorService.get_clinic_doctors(clinic_id) \ No newline at end of file diff --git a/services/clinicDoctorsServices.py b/services/clinicDoctorsServices.py index 96af948..0ab9f64 100644 --- a/services/clinicDoctorsServices.py +++ b/services/clinicDoctorsServices.py @@ -53,7 +53,9 @@ class ClinicDoctorsServices: ) # check if clinic exists - await self.clinic_services.get_clinic_by_id(user["created_clinics"][0]["id"]) + await self.clinic_services.get_clinic_by_id( + user["created_clinics"][0]["id"] + ) # exclude appointmentTypes from clinic_doctor clinic_doctor_db = ClinicDoctors( @@ -129,7 +131,6 @@ class ClinicDoctorsServices: if clinic_doctor is None: raise ResourceNotFoundException("Clinic doctor not found") - # Update the existing object with new values update_data = clinic_doctor_data.model_dump(exclude_unset=True) @@ -173,7 +174,7 @@ class ClinicDoctorsServices: finally: self.db.close() - async def get_doctor_status_count(self, clinic_id:int): + async def get_doctor_status_count(self, clinic_id: int): try: # Query to count doctors by status status_counts = ( @@ -198,14 +199,42 @@ class ClinicDoctorsServices: finally: self.db.close() - async def get_clinic_doctors(self,user, limit: int, offset: int, search: str = "", sort_by: str = DEFAULT_ORDER, sort_order: str = DEFAULT_ORDER_BY): + async def get_clinic_doctors( + self, + clinic_id: int, + limit: int | None = None, + offset: int | None = None, + search: str = "", + sort_by: str = DEFAULT_ORDER, + sort_order: str = DEFAULT_ORDER_BY, + ): + try: + response = await self._get_clinic_doctors( + clinic_id, limit, offset, search, sort_by, sort_order + ) + + return response + except Exception as e: + self.logger.error(e) + raise e + + async def _get_clinic_doctors( + self, + clinic_id: int, + limit: int | None = None, + offset: int | None = None, + search: str = "", + sort_by: str = DEFAULT_ORDER, + sort_order: str = DEFAULT_ORDER_BY, + ): try: clinic_doctors_query = ( self.db.query(ClinicDoctors) - .filter(ClinicDoctors.clinic_id == user["created_clinics"][0]["id"]) + .filter(ClinicDoctors.clinic_id == clinic_id) .options( - selectinload(ClinicDoctors.appointmentRelations) - .selectinload(AppointmentRelations.masterAppointmentTypes) + selectinload(ClinicDoctors.appointmentRelations).selectinload( + AppointmentRelations.masterAppointmentTypes + ) ) .order_by( getattr(ClinicDoctors, sort_by).desc() @@ -224,13 +253,16 @@ class ClinicDoctorsServices: ClinicDoctors.appointmentRelations.any( AppointmentRelations.masterAppointmentTypes.has( MasterAppointmentTypes.type.ilike(f"%{search}%") - ) ) - ) + ), + ) ) total = clinic_doctors_query.count() - clinic_doctors = clinic_doctors_query.limit(limit).offset(offset).all() + if limit and offset: + clinic_doctors_query = clinic_doctors_query.limit(limit).offset(offset) + + clinic_doctors = clinic_doctors_query.all() # Build response data manually to include appointment types response_data = [] @@ -244,7 +276,7 @@ class ClinicDoctorsServices: id=relation.masterAppointmentTypes.id, type=relation.masterAppointmentTypes.type, create_time=relation.masterAppointmentTypes.create_time, - update_time=relation.masterAppointmentTypes.update_time + update_time=relation.masterAppointmentTypes.update_time, ) ) @@ -256,7 +288,7 @@ class ClinicDoctorsServices: status=clinic_doctor.status, create_time=clinic_doctor.create_time, update_time=clinic_doctor.update_time, - appointmentTypes=appointment_types + appointmentTypes=appointment_types, ) response_data.append(clinic_doctor_data) diff --git a/services/clinicServices.py b/services/clinicServices.py index ea85494..0bdc30a 100644 --- a/services/clinicServices.py +++ b/services/clinicServices.py @@ -128,6 +128,23 @@ class ClinicServices: finally: self.db.close() + + async def get_clinic_by_phone(self, phone: str): + try: + clinic = self.db.query(Clinics).filter(Clinics.phone == phone).first() + + if clinic is None: + raise ResourceNotFoundException("Clinic not found") + + clinic_response = Clinic.model_validate(clinic) + + return clinic_response.model_dump( + exclude={"creator", "abn_doc", "contract_doc", "logo"} + ) + except Exception as e: + DBExceptionHandler.handle_exception(e, context="getting clinic by phone") + finally: + self.db.close() async def get_clinic_files(self, clinic_id: int):