Initial commit: InfoUser Monitor with FastAPI, agent, Docker, JWT auth, notifications, filters and CSV export

This commit is contained in:
2026-07-16 11:53:51 +07:00
commit cf2727da2b
31 changed files with 1957 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
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}