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>
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
import io
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.db.session import get_db
|
|
from app.dependencies import get_user_id
|
|
from app.models.analysis import Analysis
|
|
from app.services.storage import storage
|
|
|
|
router = APIRouter(tags=["reports"])
|
|
logger = logging.getLogger("olivas.reports")
|
|
|
|
|
|
@router.get("/analyses/{analysis_id}/report")
|
|
async def download_report(
|
|
analysis_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
user_id: str = Depends(get_user_id),
|
|
):
|
|
stmt = (
|
|
select(Analysis)
|
|
.options(selectinload(Analysis.aois))
|
|
.where(Analysis.id == analysis_id, Analysis.user_id == user_id)
|
|
)
|
|
result = await db.execute(stmt)
|
|
analysis = result.scalar_one_or_none()
|
|
if not analysis:
|
|
raise HTTPException(status_code=404, detail="Analysis not found")
|
|
|
|
if analysis.status != "completed":
|
|
raise HTTPException(status_code=400, detail="Analysis not yet completed")
|
|
|
|
# Check if PDF already cached
|
|
if storage.exists(analysis_id, "report.pdf"):
|
|
data = await storage.load_bytes(analysis_id, "report.pdf")
|
|
else:
|
|
from app.services.report_generator import generate_report
|
|
|
|
# Load images for the report
|
|
try:
|
|
original_data = await storage.load_bytes(analysis_id, "original.png")
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Original image not found")
|
|
|
|
try:
|
|
heatmap_data = await storage.load_bytes(analysis_id, "heatmap_overlay.png")
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Heatmap image not yet generated")
|
|
|
|
try:
|
|
gaze_data = await storage.load_bytes(analysis_id, "gaze_sequence.png")
|
|
except FileNotFoundError:
|
|
raise HTTPException(status_code=404, detail="Gaze sequence image not yet generated")
|
|
|
|
# Generate rule-based insights
|
|
from app.services.insights import generate_insights
|
|
rule_insights = generate_insights(analysis) if analysis.status == "completed" else []
|
|
|
|
try:
|
|
data = generate_report(
|
|
analysis=analysis,
|
|
original_image=original_data,
|
|
heatmap_image=heatmap_data,
|
|
gaze_image=gaze_data,
|
|
aois=list(analysis.aois),
|
|
rule_insights=rule_insights,
|
|
ai_insights=analysis.ai_insights,
|
|
ai_cost_usd=analysis.ai_cost_usd,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Report generation failed for {analysis_id}: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=f"Report generation failed: {str(e)}")
|
|
|
|
await storage.save_bytes(data, analysis_id, "report.pdf")
|
|
|
|
safe_name = analysis.name.replace(" ", "_").replace("/", "_")
|
|
return StreamingResponse(
|
|
io.BytesIO(data),
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="olivas-report-{safe_name}.pdf"'
|
|
},
|
|
)
|