● Zion Boggan
repos/Sightline/backend/app/api/routes/watchlists.py
zionboggan.com ↗
111 lines · python
History for this file →
1
from __future__ import annotations
2
 
3
from fastapi import APIRouter, Depends, HTTPException
4
from sqlalchemy import select
5
from sqlalchemy.orm import Session
6
 
7
from app.api.deps import Perm, get_db, require
8
from app.models import (
9
    Alert, AuditLog, PlateRead, Sighting, User, Watchlist, WatchlistEntry, WatchlistHit,
10
)
11
from app.schemas import WatchlistCreate, WatchlistEntryCreate
12
from app.services.watchlist import WatchEntry, match_all
13
 
14
router = APIRouter(tags=["watchlists"])
15
 
16
 
17
@router.get("/watchlists")
18
def list_watchlists(db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))):
19
    return db.scalars(select(Watchlist).order_by(Watchlist.created_at.desc())).all()
20
 
21
 
22
@router.post("/watchlists", status_code=201)
23
def create_watchlist(payload: WatchlistCreate, db: Session = Depends(get_db),
24
                     user: User = Depends(require(Perm.MANAGE_WATCHLIST))):
25
    wl = Watchlist(name=payload.name, kind=payload.kind, organization=payload.organization,
26
                   created_by=user.id)
27
    db.add(wl)
28
    db.flush()
29
    db.add(AuditLog(actor_user_id=user.id, action="watchlist.create",
30
                    entity_type="watchlist", entity_id=wl.id))
31
    db.commit()
32
    db.refresh(wl)
33
    return wl
34
 
35
 
36
@router.post("/watchlists/{wl_id}/entries", status_code=201)
37
def add_entry(wl_id: str, payload: WatchlistEntryCreate, backfill: bool = True,
38
              db: Session = Depends(get_db), user: User = Depends(require(Perm.MANAGE_WATCHLIST))):
39
    if not db.get(Watchlist, wl_id):
40
        raise HTTPException(404, "watchlist not found")
41
    norm = payload.plate_norm
42
    if norm:
43
        norm = "".join(c for c in norm.upper() if c.isalnum())
44
    entry = WatchlistEntry(watchlist_id=wl_id, plate_norm=norm,
45
                           partial_plate=payload.partial_plate, vehicle_desc=payload.vehicle_desc,
46
                           reason=payload.reason, expires_at=payload.expires_at)
47
    db.add(entry)
48
    db.flush()
49
 
50
    created_hits = 0
51
    if backfill:
52
        we = WatchEntry(id=entry.id, plate_norm=entry.plate_norm,
53
                        partial_plate=entry.partial_plate, vehicle_desc=entry.vehicle_desc)
54
        reads = db.scalars(select(PlateRead)).all()
55
        for r in reads:
56
            if match_all([we], r.plate_norm, None):
57
                s = db.scalar(select(Sighting).where(Sighting.plate_read_id == r.id))
58
                db.add(WatchlistHit(watchlist_entry_id=entry.id, plate_read_id=r.id,
59
                                    sighting_id=s.id if s else None, matched_on="backfill"))
60
                created_hits += 1
61
    db.add(AuditLog(actor_user_id=user.id, action="watchlist.add_entry",
62
                    entity_type="watchlist_entry", entity_id=entry.id,
63
                    meta={"backfill_hits": created_hits}))
64
    db.commit()
65
    return {"id": entry.id, "backfill_hits": created_hits}
66
 
67
 
68
@router.get("/watchlists/{wl_id}/entries")
69
def list_entries(wl_id: str, db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))):
70
    if not db.get(Watchlist, wl_id):
71
        raise HTTPException(404, "watchlist not found")
72
    rows = db.scalars(select(WatchlistEntry).where(WatchlistEntry.watchlist_id == wl_id)
73
                      .order_by(WatchlistEntry.created_at.desc())).all()
74
    return [{"id": e.id, "plate_norm": e.plate_norm, "partial_plate": e.partial_plate,
75
             "vehicle_desc": e.vehicle_desc, "reason": e.reason,
76
             "expires_at": e.expires_at, "created_at": e.created_at} for e in rows]
77
 
78
 
79
@router.get("/watchlist-hits")
80
def list_hits(db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))):
81
    rows = db.execute(
82
        select(WatchlistHit.id, WatchlistHit.matched_on, WatchlistHit.created_at,
83
               PlateRead.plate_norm, PlateRead.camera_id)
84
        .join(PlateRead, PlateRead.id == WatchlistHit.plate_read_id)
85
        .order_by(WatchlistHit.created_at.desc()).limit(200)
86
    ).all()
87
    return [{"id": r[0], "matched_on": r[1], "at": r[2], "plate_norm": r[3], "camera_id": r[4]}
88
            for r in rows]
89
 
90
 
91
@router.get("/alerts")
92
def list_alerts(status: str | None = None, db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))):
93
    q = select(Alert).order_by(Alert.created_at.desc()).limit(200)
94
    if status:
95
        q = q.where(Alert.status == status)
96
    return db.scalars(q).all()
97
 
98
 
99
@router.patch("/alerts/{alert_id}")
100
def update_alert(alert_id: int, status: str, db: Session = Depends(get_db),
101
                 user: User = Depends(require(Perm.WRITE))):
102
    alert = db.get(Alert, alert_id)
103
    if not alert:
104
        raise HTTPException(404, "alert not found")
105
    if status not in {"new", "ack", "resolved"}:
106
        raise HTTPException(400, "invalid status")
107
    alert.status = status
108
    db.add(AuditLog(actor_user_id=user.id, action="alert.update",
109
                    entity_type="alert", entity_id=str(alert_id), meta={"status": status}))
110
    db.commit()
111
    return {"id": alert_id, "status": status}