Files
security-alert-center/backend/app/api/v1/stream.py
T

89 lines
2.8 KiB
Python

import asyncio
import json
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Cookie, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.auth.jwt_auth import verify_access_token
from app.auth.stream_cookie import STREAM_COOKIE_NAME
from app.database import SessionLocal, get_db
from app.models import Event, Problem
from app.config import get_settings
from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts
from app.version import APP_VERSION
router = APIRouter(prefix="/stream", tags=["stream"])
SSE_INTERVAL_SEC = 5
def _dashboard_snapshot(db: Session) -> dict:
since = datetime.now(timezone.utc) - timedelta(hours=24)
events_24h = db.scalar(
select(func.count()).select_from(Event).where(Event.received_at >= since)
) or 0
problems_open = (
db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0
)
last_event = db.scalar(select(Event.id).order_by(Event.received_at.desc()).limit(1))
settings = get_settings()
return {
"type": "dashboard",
"version": APP_VERSION,
"events_last_24h": events_24h,
"hosts_stale": count_stale_hosts(db, settings.sac_heartbeat_stale_minutes),
"heartbeats_24h": db.scalar(
select(func.count())
.select_from(Event)
.where(Event.received_at >= since, Event.type == HEARTBEAT_TYPE)
)
or 0,
"daily_reports_24h": db.scalar(
select(func.count())
.select_from(Event)
.where(Event.received_at >= since, Event.type.in_(DAILY_REPORT_TYPES))
)
or 0,
"problems_open": problems_open,
"last_event_id": last_event,
"at": datetime.now(timezone.utc).isoformat(),
}
async def _sse_generator():
try:
while True:
db = SessionLocal()
try:
payload = _dashboard_snapshot(db)
finally:
db.close()
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
await asyncio.sleep(SSE_INTERVAL_SEC)
except asyncio.CancelledError:
return
def _sse_auth(
sac_stream: str | None = Cookie(None, alias=STREAM_COOKIE_NAME),
db: Session = Depends(get_db),
) -> str:
token = (sac_stream or "").strip()
if not token:
raise HTTPException(status_code=401, detail="SSE authentication required")
return verify_access_token(token, db=db)
@router.get("/events")
async def stream_events(
_user: str = Depends(_sse_auth),
) -> StreamingResponse:
return StreamingResponse(
_sse_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)