feat(api): production security, cors hardening, latency optimizations, postman artifacts & frontend documentation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@@ -36,6 +37,10 @@ from app.services.result_extraction_service import (
|
||||
from app.services.voice_service import VoiceService
|
||||
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.security import get_auth_context, AuthContext, check_rate_limit
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/v1/onboarding",
|
||||
tags=["Patient Onboarding"],
|
||||
@@ -48,14 +53,22 @@ router = APIRouter(
|
||||
)
|
||||
async def start_onboarding(
|
||||
request: StartOnboardingRequest,
|
||||
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=30, window_sec=60)
|
||||
|
||||
onboarding_service = OnboardingService()
|
||||
session_id = f"ONB-{uuid4().hex[:8].upper()}"
|
||||
|
||||
# Use authenticated patient identity
|
||||
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,
|
||||
@@ -108,16 +121,24 @@ async def start_onboarding(
|
||||
async def send_message(
|
||||
session_id: str,
|
||||
request: SendMessageRequest,
|
||||
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=30, window_sec=60)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
onboarding_service = OnboardingService()
|
||||
ai_service = AIOnboardingService()
|
||||
|
||||
# 1. Get session
|
||||
t_sess_start = time.perf_counter()
|
||||
session = onboarding_service.get_session(
|
||||
db,
|
||||
session_id,
|
||||
)
|
||||
t_sess_end = time.perf_counter()
|
||||
|
||||
if not session:
|
||||
raise HTTPException(
|
||||
@@ -125,6 +146,13 @@ async def send_message(
|
||||
detail="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 access or modify this patient onboarding session.",
|
||||
)
|
||||
|
||||
# 2. Check session
|
||||
if session.status != "in_progress":
|
||||
raise HTTPException(
|
||||
@@ -142,12 +170,15 @@ async def send_message(
|
||||
)
|
||||
|
||||
# 4. Get state
|
||||
t_state_start = time.perf_counter()
|
||||
state = onboarding_service.get_state(
|
||||
db,
|
||||
session_id,
|
||||
)
|
||||
t_state_end = time.perf_counter()
|
||||
|
||||
# 5. Get conversation history
|
||||
t_hist_start = time.perf_counter()
|
||||
messages = (
|
||||
db.query(OnboardingMessage)
|
||||
.filter(
|
||||
@@ -167,14 +198,17 @@ async def send_message(
|
||||
}
|
||||
for message in messages
|
||||
]
|
||||
t_hist_end = time.perf_counter()
|
||||
|
||||
# 6. Send to AI
|
||||
t_ai_start = time.perf_counter()
|
||||
ai_response = await ai_service.process_message(
|
||||
session=session,
|
||||
state=state,
|
||||
conversation_history=conversation_history,
|
||||
user_message=request.message,
|
||||
)
|
||||
t_ai_end = time.perf_counter()
|
||||
|
||||
# 7. Update state
|
||||
current_state = json.loads(
|
||||
@@ -205,7 +239,9 @@ async def send_message(
|
||||
if ai_response.get("completed"):
|
||||
session.status = "completed"
|
||||
|
||||
t_db_start = time.perf_counter()
|
||||
db.commit()
|
||||
t_db_end = time.perf_counter()
|
||||
|
||||
# 10. Save AI message
|
||||
onboarding_service.save_message(
|
||||
@@ -216,12 +252,20 @@ async def send_message(
|
||||
stage=session.stage,
|
||||
)
|
||||
|
||||
t_total_end = time.perf_counter()
|
||||
total_ms = round((t_total_end - t0) * 1000, 2)
|
||||
total_sec = round(t_total_end - t0, 3)
|
||||
|
||||
print(f"[PERF PROFILE - send_message] SESSION_LOOKUP: {(t_sess_end - t_sess_start)*1000:.2f}ms | STATE_LOAD: {(t_state_end - t_state_start)*1000:.2f}ms | HIST_LOAD: {(t_hist_end - t_hist_start)*1000:.2f}ms | AI_PROCESS: {(t_ai_end - t_ai_start)*1000:.2f}ms | DB_COMMIT: {(t_db_end - t_db_start)*1000:.2f}ms | TOTAL: {total_ms}ms")
|
||||
|
||||
return SendMessageResponse(
|
||||
success=True,
|
||||
session_id=session_id,
|
||||
stage=session.stage,
|
||||
message=ai_response["reply"],
|
||||
status=session.status,
|
||||
response_time_ms=total_ms,
|
||||
response_time_sec=total_sec,
|
||||
)
|
||||
|
||||
|
||||
@@ -232,8 +276,10 @@ async def send_message(
|
||||
async def complete_onboarding(
|
||||
session_id: str,
|
||||
request: CompleteOnboardingRequest,
|
||||
auth: AuthContext = Depends(get_auth_context),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
start_time = time.time()
|
||||
onboarding_service = OnboardingService()
|
||||
completion_service = CompletionService()
|
||||
result_service = ResultExtractionService()
|
||||
@@ -251,6 +297,13 @@ async def complete_onboarding(
|
||||
detail="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 onboarding session.",
|
||||
)
|
||||
|
||||
# 2. Load state
|
||||
state = onboarding_service.get_state(
|
||||
db,
|
||||
@@ -297,19 +350,35 @@ async def complete_onboarding(
|
||||
# 7. Patient confirmed -> submit to existing patient / hospital API
|
||||
submission = await submission_service.submit(result)
|
||||
|
||||
if submission.get("success"):
|
||||
# 8. Successful submission -> clean up temporary SQLite session
|
||||
db.query(OnboardingMessage).filter(OnboardingMessage.session_id == session_id).delete()
|
||||
db.query(OnboardingState).filter(OnboardingState.session_id == session_id).delete()
|
||||
db.delete(session)
|
||||
db.commit()
|
||||
if isinstance(result, dict):
|
||||
result["onboarding_completed"] = True
|
||||
|
||||
if session.status == "completed":
|
||||
return CompleteOnboardingResponse(
|
||||
success=True,
|
||||
session_id=session_id,
|
||||
status="submitted_and_cleaned",
|
||||
status="completed",
|
||||
result=result,
|
||||
message="Onboarding successfully completed, saved to patient record, and temporary session cleaned.",
|
||||
message="Onboarding session is already completed.",
|
||||
response_time_ms=round((time.time() - start_time) * 1000, 2),
|
||||
response_time_sec=round(time.time() - start_time, 3),
|
||||
)
|
||||
|
||||
if submission.get("success"):
|
||||
# Mark session completed in DB instead of hard-deleting
|
||||
session.status = "completed"
|
||||
session.stage = "confirmation"
|
||||
db.commit()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
return CompleteOnboardingResponse(
|
||||
success=True,
|
||||
session_id=session_id,
|
||||
status="completed",
|
||||
result=result,
|
||||
message="Onboarding successfully completed and saved to patient record.",
|
||||
response_time_ms=round(elapsed * 1000, 2),
|
||||
response_time_sec=round(elapsed, 3),
|
||||
)
|
||||
|
||||
# If submission failed, preserve SQLite session for retry
|
||||
|
||||
Reference in New Issue
Block a user