mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 04:31:28 +02:00
### Projects / Print Grouping - Create projects to group related prints (e.g., "Voron Build" with 50 parts) - Track progress with target count and completion percentage - Assign archives to projects via edit modal or context menu - Project cards show archive thumbnails with clickable links - Color-coded project badges on archive cards - Filter and manage projects by status (active/completed/archived) ### Full-Text Search (FTS5) - SQLite FTS5 virtual table for efficient searching - Search across print_name, filename, tags, notes, designer, filament_type - Automatic index sync with triggers for INSERT/UPDATE/DELETE ### Webhooks & API Keys - API key authentication with granular permissions - Permissions: can_read_status, can_manage_queue, can_control_printer - Secure key generation with prefix display only after creation - Settings page API Keys tab for key management - Webhook endpoints for external integrations ### Failure Analysis - Dashboard widget showing failure rate with color coding - Correlate failures with conditions (filament type, printer, time) - Top failure reasons breakdown - Weekly trend visualization ### Archive Comparison - Select 2-5 archives to compare side-by-side - Highlight differences in print settings (yellow) - Success/failure correlation insights - Modal with close via button, X, Escape, or backdrop ### CSV/Excel Export - Export archives and statistics with current filters - Support for both CSV and Excel (.xlsx) formats - openpyxl dependency added ## Bug Fixes - Fixed context menu submenu not showing (removed overflow-hidden) - Fixed project card thumbnails using correct API endpoint - Fixed EditArchiveModal to invalidate projects query on save - Fixed clipboard API fallback for HTTP contexts - Fixed archive PATCH 500 error (FTS5 index rebuild) - Fixed FastAPI trailing slash routing for projects endpoint ## UI Improvements - Context menu submenu with hover/click support - Project badge on archive cards with project color - "Go to Project" context menu item for assigned archives - Clickable project card thumbnails linking to archives - Reset Layout button moved to Stats page header
139 lines
4.0 KiB
Python
139 lines
4.0 KiB
Python
import logging
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from backend.app.core.database import get_db
|
|
from backend.app.core.auth import generate_api_key
|
|
from backend.app.models.api_key import APIKey
|
|
from backend.app.schemas.api_key import (
|
|
APIKeyCreate,
|
|
APIKeyUpdate,
|
|
APIKeyResponse,
|
|
APIKeyCreateResponse,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api-keys", tags=["api-keys"])
|
|
|
|
|
|
@router.get("/", response_model=list[APIKeyResponse])
|
|
async def list_api_keys(db: AsyncSession = Depends(get_db)):
|
|
"""List all API keys (without full key values)."""
|
|
result = await db.execute(
|
|
select(APIKey).order_by(APIKey.created_at.desc())
|
|
)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
@router.post("/", response_model=APIKeyCreateResponse)
|
|
async def create_api_key(
|
|
data: APIKeyCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a new API key.
|
|
|
|
IMPORTANT: The full API key is only returned in this response.
|
|
Store it securely - it cannot be retrieved again.
|
|
"""
|
|
# Generate the key
|
|
full_key, key_hash, key_prefix = generate_api_key()
|
|
|
|
api_key = APIKey(
|
|
name=data.name,
|
|
key_hash=key_hash,
|
|
key_prefix=key_prefix,
|
|
can_queue=data.can_queue,
|
|
can_control_printer=data.can_control_printer,
|
|
can_read_status=data.can_read_status,
|
|
printer_ids=data.printer_ids,
|
|
expires_at=data.expires_at,
|
|
)
|
|
db.add(api_key)
|
|
await db.flush()
|
|
await db.refresh(api_key)
|
|
|
|
# Return with full key (only time it's shown)
|
|
return APIKeyCreateResponse(
|
|
id=api_key.id,
|
|
name=api_key.name,
|
|
key_prefix=api_key.key_prefix,
|
|
key=full_key, # Only returned on creation
|
|
can_queue=api_key.can_queue,
|
|
can_control_printer=api_key.can_control_printer,
|
|
can_read_status=api_key.can_read_status,
|
|
printer_ids=api_key.printer_ids,
|
|
enabled=api_key.enabled,
|
|
last_used=api_key.last_used,
|
|
created_at=api_key.created_at,
|
|
expires_at=api_key.expires_at,
|
|
)
|
|
|
|
|
|
@router.get("/{key_id}", response_model=APIKeyResponse)
|
|
async def get_api_key(
|
|
key_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get an API key by ID."""
|
|
result = await db.execute(select(APIKey).where(APIKey.id == key_id))
|
|
api_key = result.scalar_one_or_none()
|
|
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
return api_key
|
|
|
|
|
|
@router.patch("/{key_id}", response_model=APIKeyResponse)
|
|
async def update_api_key(
|
|
key_id: int,
|
|
data: APIKeyUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Update an API key."""
|
|
result = await db.execute(select(APIKey).where(APIKey.id == key_id))
|
|
api_key = result.scalar_one_or_none()
|
|
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
# Update fields if provided
|
|
if data.name is not None:
|
|
api_key.name = data.name
|
|
if data.can_queue is not None:
|
|
api_key.can_queue = data.can_queue
|
|
if data.can_control_printer is not None:
|
|
api_key.can_control_printer = data.can_control_printer
|
|
if data.can_read_status is not None:
|
|
api_key.can_read_status = data.can_read_status
|
|
if data.printer_ids is not None:
|
|
api_key.printer_ids = data.printer_ids
|
|
if data.enabled is not None:
|
|
api_key.enabled = data.enabled
|
|
if data.expires_at is not None:
|
|
api_key.expires_at = data.expires_at
|
|
|
|
await db.flush()
|
|
await db.refresh(api_key)
|
|
|
|
return api_key
|
|
|
|
|
|
@router.delete("/{key_id}")
|
|
async def delete_api_key(
|
|
key_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Delete (revoke) an API key."""
|
|
result = await db.execute(select(APIKey).where(APIKey.id == key_id))
|
|
api_key = result.scalar_one_or_none()
|
|
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
await db.delete(api_key)
|
|
|
|
return {"message": "API key deleted"}
|