Files
bambuddy/backend/app/api/routes/print_log.py
T
maziggy 3887938e8f Security: add token-based auth for all media endpoints
Camera streams, snapshots, thumbnails, timelapse videos, photos, QR
  codes, and cover images served via <img>/<video> tags were previously
  unauthenticated because browser media elements cannot send Authorization
  headers. When auth is enabled, these endpoints are now protected by a
  reusable stream token (?token=xxx) obtained from POST
  /printers/camera/stream-token (requires CAMERA_VIEW permission).
2026-03-27 12:58:08 +01:00

131 lines
4.7 KiB
Python

import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import FileResponse
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import RequireCameraStreamTokenIfAuthEnabled, RequirePermissionIfAuthEnabled
from backend.app.core.config import settings
from backend.app.core.database import get_db
from backend.app.core.permissions import Permission
from backend.app.models.print_log import PrintLogEntry
from backend.app.models.user import User
from backend.app.schemas.print_log import PrintLogEntrySchema, PrintLogResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/print-log", tags=["print-log"])
@router.get("/", response_model=PrintLogResponse)
async def get_print_log(
search: str | None = None,
printer_id: int | None = None,
created_by_username: str | None = None,
status: str | None = None,
date_from: datetime | None = None,
date_to: datetime | None = None,
limit: int = Query(default=50, ge=1, le=500),
offset: int = Query(default=0, ge=0),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ),
):
"""Get the print log."""
query = select(PrintLogEntry)
count_query = select(func.count(PrintLogEntry.id))
if printer_id is not None:
query = query.where(PrintLogEntry.printer_id == printer_id)
count_query = count_query.where(PrintLogEntry.printer_id == printer_id)
if created_by_username:
query = query.where(PrintLogEntry.created_by_username == created_by_username)
count_query = count_query.where(PrintLogEntry.created_by_username == created_by_username)
if status:
query = query.where(PrintLogEntry.status == status)
count_query = count_query.where(PrintLogEntry.status == status)
if search:
query = query.where(PrintLogEntry.print_name.ilike(f"%{search}%"))
count_query = count_query.where(PrintLogEntry.print_name.ilike(f"%{search}%"))
if date_from:
query = query.where(PrintLogEntry.created_at >= date_from)
count_query = count_query.where(PrintLogEntry.created_at >= date_from)
if date_to:
query = query.where(PrintLogEntry.created_at <= date_to)
count_query = count_query.where(PrintLogEntry.created_at <= date_to)
# Get total count
total_result = await db.execute(count_query)
total = total_result.scalar() or 0
# Get paginated results
query = query.order_by(PrintLogEntry.created_at.desc()).offset(offset).limit(limit)
result = await db.execute(query)
entries = result.scalars().all()
return PrintLogResponse(
items=[
PrintLogEntrySchema(
id=e.id,
print_name=e.print_name,
printer_name=e.printer_name,
printer_id=e.printer_id,
status=e.status,
started_at=e.started_at,
completed_at=e.completed_at,
duration_seconds=e.duration_seconds,
filament_type=e.filament_type,
filament_color=e.filament_color,
filament_used_grams=e.filament_used_grams,
thumbnail_path=e.thumbnail_path,
created_by_username=e.created_by_username,
created_at=e.created_at,
)
for e in entries
],
total=total,
)
@router.get("/{entry_id}/thumbnail")
async def get_print_log_thumbnail(
entry_id: int,
db: AsyncSession = Depends(get_db),
_: None = RequireCameraStreamTokenIfAuthEnabled,
):
"""Get the thumbnail for a print log entry.
Requires a stream token query param (?token=xxx) when auth is enabled.
"""
entry = await db.get(PrintLogEntry, entry_id)
if not entry or not entry.thumbnail_path:
raise HTTPException(404, "Thumbnail not found")
thumb_path = settings.base_dir / entry.thumbnail_path
if not thumb_path.exists():
raise HTTPException(404, "Thumbnail file not found")
return FileResponse(
path=thumb_path,
media_type="image/png",
headers={"Cache-Control": "public, max-age=86400"},
)
@router.delete("/")
async def clear_print_log(
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_DELETE_ALL),
):
"""Clear the print log.
Only deletes log entries. Archives and queue items are never touched.
"""
result = await db.execute(delete(PrintLogEntry))
deleted = result.rowcount
await db.commit()
logger.info("Print log cleared: %d entries deleted", deleted)
return {"deleted": deleted}