Files
resolutionflow/backend/app/api/endpoints/supporting_data.py

203 lines
6.5 KiB
Python

import base64
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.api.deps import get_current_active_user
from app.models import User
from app.models.session import Session
from app.models.supporting_data import SessionSupportingData
from app.schemas.supporting_data import (
SupportingDataCreate,
SupportingDataUpdate,
SupportingDataResponse,
)
router = APIRouter(prefix="/sessions", tags=["supporting-data"])
MAX_ITEMS_PER_SESSION = 20
MAX_TEXT_SNIPPET_CHARS = 50_000
MAX_SCREENSHOT_RAW_BYTES = 2 * 1024 * 1024 # 2MB
async def _check_session_access(user: User, session: Session, db: AsyncSession) -> None:
"""Verify user has access to the session (owner, team admin, or super admin)."""
if user.is_super_admin:
return
if session.user_id == user.id:
return
# Team admins can only access sessions from their own team members
if user.is_team_admin and user.team_id is not None:
session_owner = await db.get(User, session.user_id)
if session_owner and session_owner.team_id == user.team_id:
return
raise HTTPException(status_code=403, detail="Access denied")
async def _get_session_or_404(session_id: UUID, db: AsyncSession) -> Session:
"""Fetch session by ID or raise 404."""
result = await db.execute(select(Session).where(Session.id == session_id))
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
return session
@router.post(
"/{session_id}/supporting-data",
response_model=SupportingDataResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_supporting_data(
session_id: UUID,
data: SupportingDataCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user),
):
"""Add a supporting data item (text snippet or screenshot) to a session."""
session = await _get_session_or_404(session_id, db)
await _check_session_access(current_user, session, db)
# Check item limit
count_result = await db.execute(
select(func.count()).select_from(SessionSupportingData).where(
SessionSupportingData.session_id == session_id
)
)
current_count = count_result.scalar() or 0
if current_count >= MAX_ITEMS_PER_SESSION:
raise HTTPException(
status_code=400,
detail=f"Maximum {MAX_ITEMS_PER_SESSION} supporting data items per session",
)
# Validate content size based on type
if data.data_type == "text_snippet":
if len(data.content) > MAX_TEXT_SNIPPET_CHARS:
raise HTTPException(
status_code=400,
detail=f"Text snippet exceeds maximum {MAX_TEXT_SNIPPET_CHARS} characters",
)
elif data.data_type == "screenshot":
try:
raw_bytes = base64.b64decode(data.content)
except Exception:
raise HTTPException(status_code=400, detail="Invalid base64 content for screenshot")
if len(raw_bytes) > MAX_SCREENSHOT_RAW_BYTES:
raise HTTPException(
status_code=400,
detail=f"Screenshot exceeds maximum {MAX_SCREENSHOT_RAW_BYTES // (1024 * 1024)}MB raw size",
)
# Auto-increment sort_order
max_order_result = await db.execute(
select(func.max(SessionSupportingData.sort_order)).where(
SessionSupportingData.session_id == session_id
)
)
max_order = max_order_result.scalar()
next_order = (max_order or 0) + 1
item = SessionSupportingData(
session_id=session_id,
account_id=session.account_id,
label=data.label,
data_type=data.data_type,
content=data.content,
content_type=data.content_type,
sort_order=next_order,
)
db.add(item)
await db.commit()
await db.refresh(item)
return item
@router.get(
"/{session_id}/supporting-data",
response_model=list[SupportingDataResponse],
)
async def list_supporting_data(
session_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user),
):
"""List all supporting data items for a session, ordered by sort_order."""
session = await _get_session_or_404(session_id, db)
await _check_session_access(current_user, session, db)
result = await db.execute(
select(SessionSupportingData)
.where(SessionSupportingData.session_id == session_id)
.order_by(SessionSupportingData.sort_order)
)
return result.scalars().all()
@router.patch(
"/{session_id}/supporting-data/{item_id}",
response_model=SupportingDataResponse,
)
async def update_supporting_data(
session_id: UUID,
item_id: UUID,
data: SupportingDataUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user),
):
"""Update a supporting data item's label or content."""
session = await _get_session_or_404(session_id, db)
await _check_session_access(current_user, session, db)
result = await db.execute(
select(SessionSupportingData).where(
SessionSupportingData.id == item_id,
SessionSupportingData.session_id == session_id,
)
)
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="Supporting data item not found")
if data.label is not None:
item.label = data.label
if data.content is not None:
item.content = data.content
await db.commit()
await db.refresh(item)
return item
@router.delete(
"/{session_id}/supporting-data/{item_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_supporting_data(
session_id: UUID,
item_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user),
):
"""Remove a supporting data item from a session."""
session = await _get_session_or_404(session_id, db)
await _check_session_access(current_user, session, db)
result = await db.execute(
select(SessionSupportingData).where(
SessionSupportingData.id == item_id,
SessionSupportingData.session_id == session_id,
)
)
item = result.scalar_one_or_none()
if not item:
raise HTTPException(status_code=404, detail="Supporting data item not found")
await db.delete(item)
await db.commit()