olivas/backend/app/api/endpoints/comparison.py
Vadym Samoilenko f217a5aea6 Add Azure AD SSO authentication for backend and frontend
Replace X-User-Id header auth with Azure AD JWT token validation.
Backend validates tokens via JWKS, frontend uses MSAL for login/token
acquisition. Adds logout button, 401 handling, and configurable
AZURE_AUTH_ENABLED toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:41:06 +00:00

136 lines
3.9 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.dependencies import get_user_id
from app.models.analysis import Analysis
from app.models.comparison import Comparison
from app.models.project import Project
router = APIRouter(tags=["comparison"])
class ComparisonCreateBody(BaseModel):
name: str
analysis_ids: list[str]
class ComparisonResponse(BaseModel):
id: str
name: str
analyses: list[dict]
comparison_data: dict | None = None
model_config = {"from_attributes": True}
@router.post(
"/projects/{project_id}/comparisons", response_model=ComparisonResponse, status_code=201
)
async def create_comparison(
project_id: str,
body: ComparisonCreateBody,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
# Verify project
stmt = select(Project).where(Project.id == project_id, Project.user_id == user_id)
result = await db.execute(stmt)
if not result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="Project not found")
if len(body.analysis_ids) < 2:
raise HTTPException(status_code=400, detail="Need at least 2 analyses to compare")
# Fetch analyses
analyses_stmt = select(Analysis).where(
Analysis.id.in_(body.analysis_ids),
Analysis.user_id == user_id,
Analysis.status == "completed",
)
analyses_result = await db.execute(analyses_stmt)
analyses = analyses_result.scalars().all()
if len(analyses) != len(body.analysis_ids):
raise HTTPException(
status_code=400, detail="Some analyses not found or not completed"
)
# Build comparison data
analyses_data = []
winner_id = None
max_score = -1
for a in analyses:
data = {
"analysis_id": a.id,
"name": a.name,
"overall_score": a.overall_score,
"top_fixation": a.gaze_sequence[0] if a.gaze_sequence else None,
}
analyses_data.append(data)
if a.overall_score and a.overall_score > max_score:
max_score = a.overall_score
winner_id = a.id
comparison_data = {
"winner": winner_id,
"score_delta": round(max_score - min(a.overall_score or 0 for a in analyses), 1),
}
comparison = Comparison(
project_id=project_id,
user_id=user_id,
name=body.name,
analysis_ids=body.analysis_ids,
comparison_data=comparison_data,
)
db.add(comparison)
await db.flush()
await db.refresh(comparison)
return ComparisonResponse(
id=comparison.id,
name=comparison.name,
analyses=analyses_data,
comparison_data=comparison_data,
)
@router.get("/comparisons/{comparison_id}", response_model=ComparisonResponse)
async def get_comparison(
comparison_id: str,
db: AsyncSession = Depends(get_db),
user_id: str = Depends(get_user_id),
):
stmt = select(Comparison).where(
Comparison.id == comparison_id, Comparison.user_id == user_id
)
result = await db.execute(stmt)
comparison = result.scalar_one_or_none()
if not comparison:
raise HTTPException(status_code=404, detail="Comparison not found")
# Fetch the analyses for display
analyses_stmt = select(Analysis).where(Analysis.id.in_(comparison.analysis_ids))
analyses_result = await db.execute(analyses_stmt)
analyses = analyses_result.scalars().all()
analyses_data = [
{
"analysis_id": a.id,
"name": a.name,
"overall_score": a.overall_score,
"top_fixation": a.gaze_sequence[0] if a.gaze_sequence else None,
}
for a in analyses
]
return ComparisonResponse(
id=comparison.id,
name=comparison.name,
analyses=analyses_data,
comparison_data=comparison.comparison_data,
)