Files
bambuddy/backend/app/core/auth.py
T
maziggy d1518083fc ## New Features
### 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
2025-12-10 17:06:43 +00:00

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}"
)