Files
bambuddy/backend/app/core/auth.py
T
maziggy 83cbac04b7 - Add interactive API Browser to Settings > API Keys
- New APIBrowser component with full OpenAPI schema integration
    - Fetches and parses /openapi.json automatically
    - Groups endpoints by API tags (printers, archives, settings, etc.)
    - Expandable endpoint sections with color-coded method badges
    - Path parameter, query parameter, and JSON body editors
    - Auto-populates request body with schema examples
    - Live API request execution with response display
    - Response shows status code, timing, and formatted JSON
    - Copy response button with clipboard fallback
    - Search to filter endpoints across all categories
    - Expand All / Collapse All buttons
    - Link to Swagger UI (/docs)

  - Two-column layout for API Keys tab
    - Left: API key management + webhook documentation
    - Right: API Browser with dedicated test key input

  - Parameter validation
    - Shows warning for missing required parameters
    - Validates before sending requests to avoid 422 errors

  - UX improvements
    - "Use in API Browser" button on newly created keys
    - Responsive layout (stacked on mobile, side-by-side on xl+)
2026-01-02 15:00:21 +01:00

106 lines
3.2 KiB
Python

import hashlib
import secrets
from datetime import datetime
from fastapi import Depends, Header, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
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: str | None = Header(None, alias="X-API-Key"),
db: AsyncSession = Depends(get_db),
) -> APIKey | None:
"""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}")