import base64 import re from auth import require_auth from database import events_collection from fastapi import APIRouter, Depends, HTTPException, Query from models.api import FilterOptionsResponse, PaginatedEventResponse router = APIRouter(dependencies=[Depends(require_auth)]) def _encode_cursor(timestamp: str, oid: str) -> str: payload = f"{timestamp}|{oid}" return base64.b64encode(payload.encode()).decode() def _decode_cursor(cursor: str) -> tuple[str, str]: try: payload = base64.b64decode(cursor.encode()).decode() timestamp, oid = payload.split("|", 1) return timestamp, oid except Exception as exc: raise HTTPException(status_code=400, detail="Invalid cursor") from exc @router.get("/events", response_model=PaginatedEventResponse) def list_events( service: str | None = None, actor: str | None = None, operation: str | None = None, result: str | None = None, start: str | None = None, end: str | None = None, search: str | None = None, cursor: str | None = None, page_size: int = Query(default=50, ge=1, le=500), ): filters = [] if service: filters.append({"service": service}) if actor: actor_safe = re.escape(actor) filters.append( { "$or": [ {"actor_display": {"$regex": actor_safe, "$options": "i"}}, {"actor_upn": {"$regex": actor_safe, "$options": "i"}}, {"actor.user.userPrincipalName": {"$regex": actor_safe, "$options": "i"}}, {"actor.user.id": actor}, ] } ) if operation: filters.append({"operation": {"$regex": re.escape(operation), "$options": "i"}}) if result: filters.append({"result": {"$regex": re.escape(result), "$options": "i"}}) if start or end: time_filter = {} if start: time_filter["$gte"] = start if end: time_filter["$lte"] = end filters.append({"timestamp": time_filter}) if search: search_safe = re.escape(search) filters.append( { "$or": [ {"raw_text": {"$regex": search_safe, "$options": "i"}}, {"display_summary": {"$regex": search_safe, "$options": "i"}}, {"actor_display": {"$regex": search_safe, "$options": "i"}}, {"target_displays": {"$elemMatch": {"$regex": search_safe, "$options": "i"}}}, {"operation": {"$regex": search_safe, "$options": "i"}}, ] } ) if cursor: try: cursor_ts, cursor_oid = _decode_cursor(cursor) except HTTPException: raise filters.append( { "$or": [ {"timestamp": {"$lt": cursor_ts}}, {"timestamp": cursor_ts, "_id": {"$lt": cursor_oid}}, ] } ) query = {"$and": filters} if filters else {} safe_page_size = max(1, min(page_size, 500)) try: total = events_collection.count_documents(query) if not cursor else -1 cursor_query = ( events_collection.find(query) .sort([("timestamp", -1), ("_id", -1)]) .limit(safe_page_size) ) events = list(cursor_query) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to query events: {exc}") from exc next_cursor = None if len(events) == safe_page_size: last = events[-1] next_cursor = _encode_cursor(last["timestamp"], str(last["_id"])) for e in events: e["_id"] = str(e["_id"]) return { "items": events, "total": total, "page_size": safe_page_size, "next_cursor": next_cursor, } @router.get("/filter-options", response_model=FilterOptionsResponse) def filter_options(limit: int = Query(default=200, ge=1, le=1000)): """ Provide distinct values for UI filters (best-effort, capped). """ safe_limit = max(1, min(limit, 1000)) try: services = sorted(events_collection.distinct("service"))[:safe_limit] operations = sorted(events_collection.distinct("operation"))[:safe_limit] results = sorted([r for r in events_collection.distinct("result") if r])[:safe_limit] actors = sorted([a for a in events_collection.distinct("actor_display") if a])[:safe_limit] actor_upns = sorted([a for a in events_collection.distinct("actor_upn") if a])[:safe_limit] devices = sorted([a for a in events_collection.distinct("target_displays") if isinstance(a, str)])[:safe_limit] except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to load filter options: {exc}") from exc return { "services": services, "operations": operations, "results": results, "actors": actors, "actor_upns": actor_upns, "devices": devices, }