"""Case state: the human layer over the alert queue.""" from __future__ import annotations import datetime as dt from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy.orm import Session from ..auth import current_user from ..db import get_db from ..models import Case, CaseStatus, User from ..services import add_event, set_status router = APIRouter(prefix="/api/cases", tags=["cases"]) class StatusBody(BaseModel): status: str note: str = "" class NoteBody(BaseModel): note: str class SnoozeBody(BaseModel): hours: int = 24 note: str = "" def _case(db: Session, fingerprint: str) -> Case: found = db.query(Case).filter(Case.fingerprint == fingerprint).one_or_none() if not found: raise HTTPException(status.HTTP_404_NOT_FOUND, "No case for that alert yet") return found @router.get("") def list_cases(open_only: bool = True, limit: int = 200, db: Session = Depends(get_db), user: User = Depends(current_user)): query = db.query(Case).order_by(Case.last_seen_at.desc()) rows = [c for c in query.limit(max(1, min(limit, 1000))).all() if (c.is_open or not open_only)] return {"cases": [c.to_json() for c in rows], "statuses": [s.value for s in CaseStatus]} @router.get("/{fingerprint}") def get_case(fingerprint: str, db: Session = Depends(get_db), user: User = Depends(current_user)): return _case(db, fingerprint).to_json(with_events=True) @router.post("/{fingerprint}/status") def change_status(fingerprint: str, body: StatusBody, db: Session = Depends(get_db), user: User = Depends(current_user)): try: new_status = CaseStatus(body.status) except ValueError as exc: raise HTTPException(status.HTTP_400_BAD_REQUEST, f"Unknown status '{body.status}'") from exc case = set_status(db, _case(db, fingerprint), new_status, user, body.note) return case.to_json(with_events=True) @router.post("/{fingerprint}/assign") def assign(fingerprint: str, db: Session = Depends(get_db), user: User = Depends(current_user)): case = _case(db, fingerprint) # Set the relationship, not just the id: the response is serialised from # this same object and a bare id leaves `assignee` null in the payload. case.assignee = user if case.status == CaseStatus.NEW: case.status = CaseStatus.INVESTIGATING add_event(db, case, user, "assigned", f"Picked up by {user.email}") db.commit() db.refresh(case) return case.to_json(with_events=True) @router.post("/{fingerprint}/note") def add_note(fingerprint: str, body: NoteBody, db: Session = Depends(get_db), user: User = Depends(current_user)): if not body.note.strip(): raise HTTPException(status.HTTP_400_BAD_REQUEST, "Empty note") case = _case(db, fingerprint) case.notes = (case.notes + "\n" if case.notes else "") + body.note.strip() add_event(db, case, user, "note", body.note.strip()) db.commit() return case.to_json(with_events=True) @router.post("/{fingerprint}/snooze") def snooze(fingerprint: str, body: SnoozeBody, db: Session = Depends(get_db), user: User = Depends(current_user)): case = _case(db, fingerprint) until = dt.datetime.now(dt.timezone.utc) + dt.timedelta(hours=max(1, body.hours)) case.snooze_until = until add_event(db, case, user, "snoozed", f"Snoozed for {body.hours}h. {body.note}".strip()) db.commit() return case.to_json(with_events=True)