feat: phase 1C JWT UI, events/hosts API, Vue frontend
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth.jwt_auth import create_access_token
|
||||
from app.config import get_settings
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(body: LoginRequest) -> TokenResponse:
|
||||
settings = get_settings()
|
||||
if not settings.sac_admin_password:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="SAC_ADMIN_PASSWORD is not configured",
|
||||
)
|
||||
if body.username != settings.sac_admin_username or body.password != settings.sac_admin_password:
|
||||
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||
token = create_access_token(body.username)
|
||||
return TokenResponse(access_token=token)
|
||||
@@ -1,12 +1,17 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from app.auth.api_key import get_api_key_auth
|
||||
from app.auth.jwt_auth import get_current_user
|
||||
from app.config import get_settings
|
||||
from app.database import get_db
|
||||
from app.models import Event, Host
|
||||
from app.schemas.list_models import EventDetail, EventListResponse, EventSummary
|
||||
from app.services.ingest import ingest_event
|
||||
from app.services.schema_validate import validate_event_payload
|
||||
|
||||
@@ -41,3 +46,109 @@ def post_event(
|
||||
created=created,
|
||||
sac_event_url=f"{base}/api/v1/events/{event.id}",
|
||||
)
|
||||
|
||||
|
||||
def _parse_optional_dt(value: str | None) -> datetime | None:
|
||||
if not value:
|
||||
return None
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
@router.get("", response_model=EventListResponse)
|
||||
def list_events(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
severity: str | None = None,
|
||||
type: str | None = None,
|
||||
host_id: int | None = None,
|
||||
hostname: str | None = None,
|
||||
from_time: str | None = Query(None, alias="from"),
|
||||
to_time: str | None = Query(None, alias="to"),
|
||||
q: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> EventListResponse:
|
||||
stmt = select(Event).join(Host).options(joinedload(Event.host))
|
||||
count_stmt = select(func.count()).select_from(Event).join(Host)
|
||||
|
||||
if severity:
|
||||
stmt = stmt.where(Event.severity == severity)
|
||||
count_stmt = count_stmt.where(Event.severity == severity)
|
||||
if type:
|
||||
stmt = stmt.where(Event.type == type)
|
||||
count_stmt = count_stmt.where(Event.type == type)
|
||||
if host_id is not None:
|
||||
stmt = stmt.where(Event.host_id == host_id)
|
||||
count_stmt = count_stmt.where(Event.host_id == host_id)
|
||||
if hostname:
|
||||
like = f"%{hostname}%"
|
||||
stmt = stmt.where(Host.hostname.ilike(like))
|
||||
count_stmt = count_stmt.where(Host.hostname.ilike(like))
|
||||
dt_from = _parse_optional_dt(from_time)
|
||||
dt_to = _parse_optional_dt(to_time)
|
||||
if dt_from:
|
||||
stmt = stmt.where(Event.occurred_at >= dt_from)
|
||||
count_stmt = count_stmt.where(Event.occurred_at >= dt_from)
|
||||
if dt_to:
|
||||
stmt = stmt.where(Event.occurred_at <= dt_to)
|
||||
count_stmt = count_stmt.where(Event.occurred_at <= dt_to)
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
stmt = stmt.where(Event.summary.ilike(like) | Event.title.ilike(like))
|
||||
count_stmt = count_stmt.where(Event.summary.ilike(like) | Event.title.ilike(like))
|
||||
|
||||
total = db.scalar(count_stmt) or 0
|
||||
rows = db.scalars(
|
||||
stmt.order_by(Event.occurred_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
).all()
|
||||
|
||||
items = [
|
||||
EventSummary(
|
||||
id=e.id,
|
||||
event_id=e.event_id,
|
||||
host_id=e.host_id,
|
||||
hostname=e.host.hostname,
|
||||
occurred_at=e.occurred_at,
|
||||
received_at=e.received_at,
|
||||
category=e.category,
|
||||
type=e.type,
|
||||
severity=e.severity,
|
||||
title=e.title,
|
||||
summary=e.summary,
|
||||
)
|
||||
for e in rows
|
||||
]
|
||||
return EventListResponse(items=items, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/{event_db_id}", response_model=EventDetail)
|
||||
def get_event(
|
||||
event_db_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> EventDetail:
|
||||
event = db.scalar(
|
||||
select(Event).where(Event.id == event_db_id).options(joinedload(Event.host))
|
||||
)
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
return EventDetail(
|
||||
id=event.id,
|
||||
event_id=event.event_id,
|
||||
host_id=event.host_id,
|
||||
hostname=event.host.hostname,
|
||||
occurred_at=event.occurred_at,
|
||||
received_at=event.received_at,
|
||||
category=event.category,
|
||||
type=event.type,
|
||||
severity=event.severity,
|
||||
title=event.title,
|
||||
summary=event.summary,
|
||||
details=event.details,
|
||||
raw=event.raw,
|
||||
dedup_key=event.dedup_key,
|
||||
correlation_id=event.correlation_id,
|
||||
payload=event.payload,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.jwt_auth import get_current_user
|
||||
from app.database import get_db
|
||||
from app.models import Event, Host
|
||||
from app.schemas.list_models import HostListResponse, HostSummary
|
||||
|
||||
router = APIRouter(prefix="/hosts", tags=["hosts"])
|
||||
|
||||
|
||||
@router.get("", response_model=HostListResponse)
|
||||
def list_hosts(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
product: str | None = None,
|
||||
hostname: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
_user: str = Depends(get_current_user),
|
||||
) -> HostListResponse:
|
||||
stmt = select(Host)
|
||||
count_stmt = select(func.count()).select_from(Host)
|
||||
|
||||
if product:
|
||||
stmt = stmt.where(Host.product == product)
|
||||
count_stmt = count_stmt.where(Host.product == product)
|
||||
if hostname:
|
||||
like = f"%{hostname}%"
|
||||
stmt = stmt.where(Host.hostname.ilike(like))
|
||||
count_stmt = count_stmt.where(Host.hostname.ilike(like))
|
||||
|
||||
total = db.scalar(count_stmt) or 0
|
||||
rows = db.scalars(
|
||||
stmt.order_by(Host.last_seen_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
).all()
|
||||
|
||||
items: list[HostSummary] = []
|
||||
for host in rows:
|
||||
event_count = db.scalar(
|
||||
select(func.count()).select_from(Event).where(Event.host_id == host.id)
|
||||
)
|
||||
items.append(
|
||||
HostSummary(
|
||||
id=host.id,
|
||||
hostname=host.hostname,
|
||||
display_name=host.display_name,
|
||||
os_family=host.os_family,
|
||||
product=host.product,
|
||||
product_version=host.product_version,
|
||||
ipv4=host.ipv4,
|
||||
last_seen_at=host.last_seen_at,
|
||||
event_count=int(event_count or 0),
|
||||
)
|
||||
)
|
||||
|
||||
return HostListResponse(items=items, total=total, page=page, page_size=page_size)
|
||||
@@ -1,7 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import events, health
|
||||
from app.api.v1 import auth, events, health, hosts
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router)
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(events.router)
|
||||
api_router.include_router(hosts.router)
|
||||
|
||||
Reference in New Issue
Block a user