mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 03:31:25 +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
115 lines
3.3 KiB
Python
115 lines
3.3 KiB
Python
import hashlib
|
|
import secrets
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import Header, HTTPException, Depends
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from backend.app.core.database import get_db
|
|
from backend.app.models.api_key import APIKey
|
|
|
|
|
|
def generate_api_key() -> tuple[str, str, str]:
|
|
"""Generate a new API key.
|
|
|
|
Returns:
|
|
Tuple of (full_key, key_hash, key_prefix)
|
|
"""
|
|
# Generate a random 32-byte key and encode as hex (64 chars)
|
|
full_key = f"bb_{secrets.token_hex(32)}"
|
|
key_hash = hashlib.sha256(full_key.encode()).hexdigest()
|
|
key_prefix = full_key[:11] # "bb_" + first 8 chars of token
|
|
return full_key, key_hash, key_prefix
|
|
|
|
|
|
def hash_api_key(key: str) -> str:
|
|
"""Hash an API key for comparison."""
|
|
return hashlib.sha256(key.encode()).hexdigest()
|
|
|
|
|
|
async def get_api_key(
|
|
x_api_key: str = Header(..., alias="X-API-Key"),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> APIKey:
|
|
"""Verify API key and return the key record.
|
|
|
|
Raises HTTPException if key is invalid, disabled, or expired.
|
|
"""
|
|
key_hash = hash_api_key(x_api_key)
|
|
|
|
result = await db.execute(
|
|
select(APIKey).where(APIKey.key_hash == key_hash)
|
|
)
|
|
api_key = result.scalar_one_or_none()
|
|
|
|
if not api_key:
|
|
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
|
|
if not api_key.enabled:
|
|
raise HTTPException(status_code=403, detail="API key is disabled")
|
|
|
|
if api_key.expires_at and api_key.expires_at < datetime.utcnow():
|
|
raise HTTPException(status_code=403, detail="API key has expired")
|
|
|
|
# Update last_used timestamp
|
|
api_key.last_used = datetime.utcnow()
|
|
|
|
return api_key
|
|
|
|
|
|
async def get_optional_api_key(
|
|
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> Optional[APIKey]:
|
|
"""Get API key if provided, return None otherwise."""
|
|
if not x_api_key:
|
|
return None
|
|
|
|
try:
|
|
return await get_api_key(x_api_key, db)
|
|
except HTTPException:
|
|
return None
|
|
|
|
|
|
def check_permission(api_key: APIKey, permission: str) -> None:
|
|
"""Check if API key has a specific permission.
|
|
|
|
Args:
|
|
api_key: The API key record
|
|
permission: One of 'queue', 'control_printer', 'read_status'
|
|
|
|
Raises HTTPException if permission is denied.
|
|
"""
|
|
permission_map = {
|
|
'queue': api_key.can_queue,
|
|
'control_printer': api_key.can_control_printer,
|
|
'read_status': api_key.can_read_status,
|
|
}
|
|
|
|
if permission not in permission_map:
|
|
raise HTTPException(status_code=500, detail=f"Unknown permission: {permission}")
|
|
|
|
if not permission_map[permission]:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=f"API key does not have '{permission}' permission"
|
|
)
|
|
|
|
|
|
def check_printer_access(api_key: APIKey, printer_id: int) -> None:
|
|
"""Check if API key has access to a specific printer.
|
|
|
|
Args:
|
|
api_key: The API key record
|
|
printer_id: The printer ID to check
|
|
|
|
Raises HTTPException if access is denied.
|
|
"""
|
|
if api_key.printer_ids is not None and printer_id not in api_key.printer_ids:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=f"API key does not have access to printer {printer_id}"
|
|
)
|