feat: new endpoints for agent

feat: new auth middleware for agent
This commit is contained in:
deepvasoya 2025-06-12 13:49:08 +05:30
parent db20c498c2
commit 6ce3e4acce
8 changed files with 134 additions and 15 deletions

View File

@ -1,11 +1,12 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from middleware.auth_dependency import auth_required from middleware.auth_dependency import auth_required
from middleware.auth_secret import verify_secret
from fastapi.security import HTTPBearer from fastapi.security import HTTPBearer
# Import the security scheme # Import the security scheme
bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") 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() 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(call_transcripts.router, prefix="/call-transcripts", tags=["call-transcripts"])
api_router.include_router(notifications.router, prefix="/notifications", tags=["notifications"], dependencies=[Depends(auth_required)]) 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)

18
apis/endpoints/agent.py Normal file
View File

@ -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)

View File

@ -62,6 +62,7 @@ async def verify_otp(data: AuthOTP):
@router.get("/is-valid-domain") @router.get("/is-valid-domain")
async def is_valid_domain(req:Request): 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) is_valid = await ClinicServices().is_valid_domain(host)
return status.HTTP_200_OK if is_valid else status.HTTP_404_NOT_FOUND return status.HTTP_200_OK if is_valid else status.HTTP_404_NOT_FOUND

View File

@ -20,7 +20,9 @@ async def get_clinic_doctors(
if page < 1: if page < 1:
page = 1 page = 1
offset = (page - 1) * limit 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") return ApiResponse(data=clinic_doctors, message="Clinic doctors retrieved successfully")
@router.post("/") @router.post("/")

29
middleware/auth_secret.py Normal file
View File

@ -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

15
services/agentServices.py Normal file
View File

@ -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)

View File

@ -53,7 +53,9 @@ class ClinicDoctorsServices:
) )
# check if clinic exists # 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 # exclude appointmentTypes from clinic_doctor
clinic_doctor_db = ClinicDoctors( clinic_doctor_db = ClinicDoctors(
@ -129,7 +131,6 @@ class ClinicDoctorsServices:
if clinic_doctor is None: if clinic_doctor is None:
raise ResourceNotFoundException("Clinic doctor not found") raise ResourceNotFoundException("Clinic doctor not found")
# Update the existing object with new values # Update the existing object with new values
update_data = clinic_doctor_data.model_dump(exclude_unset=True) update_data = clinic_doctor_data.model_dump(exclude_unset=True)
@ -173,7 +174,7 @@ class ClinicDoctorsServices:
finally: finally:
self.db.close() self.db.close()
async def get_doctor_status_count(self, clinic_id:int): async def get_doctor_status_count(self, clinic_id: int):
try: try:
# Query to count doctors by status # Query to count doctors by status
status_counts = ( status_counts = (
@ -198,14 +199,42 @@ class ClinicDoctorsServices:
finally: finally:
self.db.close() 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: try:
clinic_doctors_query = ( clinic_doctors_query = (
self.db.query(ClinicDoctors) self.db.query(ClinicDoctors)
.filter(ClinicDoctors.clinic_id == user["created_clinics"][0]["id"]) .filter(ClinicDoctors.clinic_id == clinic_id)
.options( .options(
selectinload(ClinicDoctors.appointmentRelations) selectinload(ClinicDoctors.appointmentRelations).selectinload(
.selectinload(AppointmentRelations.masterAppointmentTypes) AppointmentRelations.masterAppointmentTypes
)
) )
.order_by( .order_by(
getattr(ClinicDoctors, sort_by).desc() getattr(ClinicDoctors, sort_by).desc()
@ -224,13 +253,16 @@ class ClinicDoctorsServices:
ClinicDoctors.appointmentRelations.any( ClinicDoctors.appointmentRelations.any(
AppointmentRelations.masterAppointmentTypes.has( AppointmentRelations.masterAppointmentTypes.has(
MasterAppointmentTypes.type.ilike(f"%{search}%") MasterAppointmentTypes.type.ilike(f"%{search}%")
)
) )
) ),
)
) )
total = clinic_doctors_query.count() 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 # Build response data manually to include appointment types
response_data = [] response_data = []
@ -244,7 +276,7 @@ class ClinicDoctorsServices:
id=relation.masterAppointmentTypes.id, id=relation.masterAppointmentTypes.id,
type=relation.masterAppointmentTypes.type, type=relation.masterAppointmentTypes.type,
create_time=relation.masterAppointmentTypes.create_time, 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, status=clinic_doctor.status,
create_time=clinic_doctor.create_time, create_time=clinic_doctor.create_time,
update_time=clinic_doctor.update_time, update_time=clinic_doctor.update_time,
appointmentTypes=appointment_types appointmentTypes=appointment_types,
) )
response_data.append(clinic_doctor_data) response_data.append(clinic_doctor_data)

View File

@ -129,6 +129,23 @@ class ClinicServices:
self.db.close() 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): async def get_clinic_files(self, clinic_id: int):
try: try: