| 1 | from __future__ import annotations |
| 2 | |
| 3 | from fastapi import APIRouter, Depends, Query |
| 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.core.config import settings |
| 9 | from app.models import AuditLog, Source, User |
| 10 | from app.schemas import SourceCreate, UserOut |
| 11 | |
| 12 | router = APIRouter(tags=["admin"]) |
| 13 | |
| 14 | |
| 15 | @router.get("/sources") |
| 16 | def list_sources(db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))): |
| 17 | return db.scalars(select(Source).order_by(Source.created_at.desc())).all() |
| 18 | |
| 19 | |
| 20 | @router.post("/sources", status_code=201) |
| 21 | def create_source(payload: SourceCreate, db: Session = Depends(get_db), |
| 22 | user: User = Depends(require(Perm.INGEST))): |
| 23 | src = Source(name=payload.name, system_type=payload.system_type, |
| 24 | organization=payload.organization, jurisdiction=payload.jurisdiction, |
| 25 | trust_level=payload.trust_level) |
| 26 | db.add(src) |
| 27 | db.commit() |
| 28 | db.refresh(src) |
| 29 | return src |
| 30 | |
| 31 | |
| 32 | @router.get("/users", response_model=list[UserOut]) |
| 33 | def list_users(db: Session = Depends(get_db), _: User = Depends(require(Perm.ADMIN))): |
| 34 | return db.scalars(select(User)).all() |
| 35 | |
| 36 | |
| 37 | @router.get("/audit") |
| 38 | def audit_log(entity_type: str | None = None, entity_id: str | None = None, |
| 39 | actor: str | None = None, limit: int = Query(default=200, le=1000), |
| 40 | db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))): |
| 41 | q = select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit) |
| 42 | if entity_type: |
| 43 | q = q.where(AuditLog.entity_type == entity_type) |
| 44 | if entity_id: |
| 45 | q = q.where(AuditLog.entity_id == entity_id) |
| 46 | if actor: |
| 47 | q = q.where(AuditLog.actor_user_id == actor) |
| 48 | rows = db.scalars(q).all() |
| 49 | return [{"id": r.id, "actor": r.actor_user_id, "action": r.action, |
| 50 | "entity_type": r.entity_type, "entity_id": r.entity_id, |
| 51 | "metadata": r.meta, "at": r.created_at} for r in rows] |
| 52 | |
| 53 | |
| 54 | @router.post("/admin/retention/run") |
| 55 | def run_retention(dry_run: bool = True, db: Session = Depends(get_db), |
| 56 | user: User = Depends(require(Perm.ADMIN))): |
| 57 | """Enforce the retention window. dry_run reports how many reads WOULD be deleted without |
| 58 | deleting; set dry_run=false to actually purge. Always audited.""" |
| 59 | from datetime import datetime, timedelta, timezone |
| 60 | from app.services.retention import count_expired, purge_expired |
| 61 | cutoff = datetime.now(timezone.utc) - timedelta(days=settings.default_retention_days) |
| 62 | count = count_expired(db, cutoff) |
| 63 | deleted = 0 |
| 64 | if not dry_run: |
| 65 | deleted = purge_expired(db, cutoff) |
| 66 | db.add(AuditLog(actor_user_id=user.id, action="retention.run", |
| 67 | entity_type="settings", entity_id=None, |
| 68 | meta={"dry_run": dry_run, "cutoff": cutoff.isoformat(), |
| 69 | "candidates": count, "deleted": deleted})) |
| 70 | db.commit() |
| 71 | return {"cutoff": cutoff.isoformat(), "retention_days": settings.default_retention_days, |
| 72 | "candidates": count, "deleted": deleted, "dry_run": dry_run} |
| 73 | |
| 74 | |
| 75 | @router.get("/admin/settings") |
| 76 | def get_settings(_: User = Depends(require(Perm.ADMIN))): |
| 77 | return { |
| 78 | "default_retention_days": settings.default_retention_days, |
| 79 | "rate_limit_per_minute": settings.rate_limit_per_minute, |
| 80 | "dedup_window_seconds": settings.dedup_window_seconds, |
| 81 | "impossible_speed_kmh": settings.impossible_speed_kmh, |
| 82 | "cotravel_window_seconds": settings.cotravel_window_seconds, |
| 83 | "environment": settings.environment, |
| 84 | } |