feat(api): production security, cors hardening, latency optimizations, postman artifacts & frontend documentation

This commit is contained in:
2026-08-31 13:48:54 +05:30
parent 4e21d617ce
commit e91cdbae6e
48 changed files with 3742 additions and 65 deletions
@@ -5,7 +5,7 @@ import time
from uuid import uuid4
from typing import Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
@@ -32,6 +32,8 @@ from app.services.patient_submission_service import PatientSubmissionService
from app.services.result_extraction_service import ResultExtractionService
from app.services.voice_service import VoiceService
from app.core.security import get_auth_context, AuthContext, check_rate_limit
router = APIRouter(
prefix="/v1/voice-onboarding",
tags=["Voice Onboarding"],
@@ -44,8 +46,12 @@ router = APIRouter(
)
async def start_voice_onboarding(
request: StartVoiceOnboardingRequest,
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=15, window_sec=60)
start_time = time.time()
try:
onboarding_service = OnboardingService()
@@ -60,9 +66,11 @@ async def start_voice_onboarding(
name="General Medicine / Primary Care (Auto-Triage)",
)
patient_id = request.patient.id or auth.patient_id
session = OnboardingSession(
session_id=session_id,
patient_id=request.patient.id,
patient_id=patient_id,
patient_name=request.patient.name,
age=request.patient.age,
gender=request.patient.gender,
@@ -131,8 +139,12 @@ async def voice_turn(
session_id: str,
audio: Optional[UploadFile] = File(None),
text_fallback: Optional[str] = Form(None),
req_obj: Request = None,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
if req_obj and req_obj.client:
check_rate_limit(req_obj.client.host, limit=20, window_sec=60)
start_time = time.time()
onboarding_service = OnboardingService()
voice_service = VoiceService()
@@ -146,10 +158,21 @@ async def voice_turn(
detail="Voice onboarding session not found",
)
if session.status != "in_progress":
# 1b. IDOR Session Ownership Check
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to access or modify this voice session.",
)
if session.status == "completed":
session.status = "in_progress"
db.commit()
if session.status not in ["in_progress", "completed"]:
raise HTTPException(
status_code=400,
detail="Voice onboarding session is not active",
detail="Voice onboarding session is closed or submitted.",
)
# 2. Fast-path transcript: use real-time Web Speech text_fallback first for zero-latency processing
@@ -244,6 +267,11 @@ async def voice_turn(
# 6. Update Structured Clinical State
current_state = json.loads(state.state_json)
updates = ai_response.get("state_updates", {})
if hasattr(updates, "model_dump"):
updates = updates.model_dump()
elif not isinstance(updates, dict):
updates = {}
for k, v in updates.items():
if v is not None:
if isinstance(v, list) and len(v) == 0:
@@ -348,6 +376,7 @@ async def get_audio(audio_id: str):
async def complete_voice_onboarding(
session_id: str,
request: CompleteOnboardingRequest,
auth: AuthContext = Depends(get_auth_context),
db: Session = Depends(get_db),
):
start_time = time.time()
@@ -363,6 +392,13 @@ async def complete_voice_onboarding(
detail="Voice onboarding session not found",
)
# 1b. Verify Session Ownership (IDOR Prevention)
if session.patient_id != auth.patient_id and auth.role != "admin":
raise HTTPException(
status_code=403,
detail="Forbidden: You do not have authorization to complete this voice session.",
)
state = onboarding_service.get_state(db, session_id)
# Check if there is any conversation to summarize
@@ -389,6 +425,20 @@ async def complete_voice_onboarding(
resp_ms = round(elapsed * 1000, 2)
resp_sec = round(elapsed, 3)
if isinstance(result, dict):
result["onboarding_completed"] = True
if session.status == "completed":
return CompleteOnboardingResponse(
success=True,
session_id=session_id,
status="completed",
result=result,
message="Voice onboarding session is already completed.",
response_time_ms=resp_ms,
response_time_sec=resp_sec,
)
if not request.confirmed:
return CompleteOnboardingResponse(
success=True,
@@ -403,15 +453,14 @@ async def complete_voice_onboarding(
submission = await submission_service.submit(result)
if submission.get("success"):
db.query(OnboardingMessage).filter(OnboardingMessage.session_id == session_id).delete()
db.query(OnboardingState).filter(OnboardingState.session_id == session_id).delete()
db.delete(session)
session.status = "completed"
session.stage = "confirmation"
db.commit()
return CompleteOnboardingResponse(
success=True,
session_id=session_id,
status="submitted_and_cleaned",
status="completed",
result=result,
message="Voice onboarding successfully saved to patient record.",
response_time_ms=resp_ms,