import csv import io import json from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from sqlalchemy import desc, asc, or_ from datetime import datetime, timezone, timedelta from typing import List, Optional from config import get_settings from database import get_db from models import Computer, Heartbeat, Notification from schemas import HeartbeatPayload, ComputerOut, ComputerDetailOut, HeartbeatOut, NotificationOut from auth import get_current_user from notifications import check_offline_computers, mark_notification_read router = APIRouter(prefix="/api", tags=["api"]) settings = get_settings() def get_agent_token(request: Request) -> str: auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing agent token") return auth[7:] @router.post("/heartbeat") def heartbeat( payload: HeartbeatPayload, request: Request, db: Session = Depends(get_db), ): token = get_agent_token(request) if token != settings.AGENT_TOKEN: raise HTTPException(status_code=401, detail="Invalid agent token") now = datetime.now(timezone.utc) computer = db.query(Computer).filter(Computer.hostname == payload.hostname).first() if not computer: computer = Computer( hostname=payload.hostname, os_info=payload.os_info, first_seen=now, ) db.add(computer) db.commit() db.refresh(computer) computer.last_seen = now computer.status = "online" computer.current_user = payload.username computer.current_ip = payload.local_ip computer.current_uptime_sec = payload.uptime_sec computer.current_cpu_percent = payload.cpu_percent computer.current_ram_percent = payload.ram_percent computer.current_ram_total_gb = payload.ram_total_gb heartbeat = Heartbeat( computer_id=computer.id, timestamp=now, username=payload.username, local_ip=payload.local_ip, public_ip=payload.public_ip, uptime_sec=payload.uptime_sec, cpu_percent=payload.cpu_percent, ram_percent=payload.ram_percent, ram_total_gb=payload.ram_total_gb, disk_info=payload.disk_info or [], cpu_temp=payload.cpu_temp, load_avg=payload.load_avg, processes=payload.processes or [], ) db.add(heartbeat) db.commit() return {"ok": True, "computer_id": computer.id} @router.get("/computers", response_model=List[ComputerOut]) def list_computers( status: Optional[str] = None, user: Optional[str] = None, search: Optional[str] = None, sort: str = "hostname", order: str = "asc", db: Session = Depends(get_db), current_user=Depends(get_current_user), ): # Mark old computers as offline and create notifications check_offline_computers(db) query = db.query(Computer) if status and status in ("online", "offline"): query = query.filter(Computer.status == status) if user: query = query.filter(Computer.current_user.ilike(f"%{user}%")) if search: query = query.filter( or_( Computer.hostname.ilike(f"%{search}%"), Computer.os_info.ilike(f"%{search}%"), Computer.current_ip.ilike(f"%{search}%"), ) ) allowed_sort = {"hostname", "last_seen", "current_user", "status", "current_cpu_percent", "current_ram_percent"} if sort in allowed_sort: column = getattr(Computer, sort) query = query.order_by(asc(column) if order == "asc" else desc(column)) else: query = query.order_by(asc(Computer.hostname)) return query.all() @router.get("/computers/{computer_id}", response_model=ComputerDetailOut) def get_computer( computer_id: int, hours: int = 24, db: Session = Depends(get_db), user=Depends(get_current_user), ): computer = db.query(Computer).filter(Computer.id == computer_id).first() if not computer: raise HTTPException(status_code=404, detail="Computer not found") since = datetime.now(timezone.utc) - timedelta(hours=hours) heartbeats = ( db.query(Heartbeat) .filter(Heartbeat.computer_id == computer_id, Heartbeat.timestamp >= since) .order_by(Heartbeat.timestamp) .all() ) computer.heartbeats = heartbeats return computer @router.get("/computers/export/csv") def export_csv( status: Optional[str] = None, user: Optional[str] = None, search: Optional[str] = None, db: Session = Depends(get_db), current_user=Depends(get_current_user), ): query = db.query(Computer) if status and status in ("online", "offline"): query = query.filter(Computer.status == status) if user: query = query.filter(Computer.current_user.ilike(f"%{user}%")) if search: query = query.filter( or_( Computer.hostname.ilike(f"%{search}%"), Computer.os_info.ilike(f"%{search}%"), Computer.current_ip.ilike(f"%{search}%"), ) ) computers = query.order_by(Computer.hostname).all() output = io.StringIO() writer = csv.writer(output) writer.writerow([ "ID", "Hostname", "User", "IP", "Status", "OS", "CPU %", "RAM %", "RAM Total GB", "Uptime sec", "Last seen", "First seen", ]) for c in computers: writer.writerow([ c.id, c.hostname, c.current_user or "", c.current_ip or "", c.status, c.os_info or "", c.current_cpu_percent or "", c.current_ram_percent or "", c.current_ram_total_gb or "", c.current_uptime_sec or "", c.last_seen.isoformat() if c.last_seen else "", c.first_seen.isoformat() if c.first_seen else "", ]) output.seek(0) filename = f"computers-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.csv" return StreamingResponse( io.BytesIO(output.getvalue().encode("utf-8-sig")), media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f"attachment; filename={filename}"}, ) @router.get("/notifications", response_model=List[NotificationOut]) def list_notifications( unread_only: bool = False, limit: int = 50, db: Session = Depends(get_db), user=Depends(get_current_user), ): query = db.query(Notification) if unread_only: query = query.filter(Notification.is_read == False) return query.order_by(desc(Notification.sent_at)).limit(limit).all() @router.post("/notifications/{notification_id}/read") def read_notification( notification_id: int, db: Session = Depends(get_db), user=Depends(get_current_user), ): if mark_notification_read(db, notification_id): return {"ok": True} raise HTTPException(status_code=404, detail="Notification not found") @router.post("/notifications/read-all") def read_all_notifications( db: Session = Depends(get_db), user=Depends(get_current_user), ): db.query(Notification).filter(Notification.is_read == False).update( {"is_read": True, "read_at": datetime.now(timezone.utc)} ) db.commit() return {"ok": True}