Files
maziggy 6855d65d12 Let other applications send messages through the notification channels
POST /notifications/app-message delivers an app's message to every channel
with the new "Messages from connected apps" switch on (off by default),
through quiet hours, the digest and the log. API keys need the new "Send
notifications" permission, and their owner notifications:update; plain text,
http(s) links, 20 messages a minute per key. The electricity-price door and
this one now share one scoped-key check. /queue?batch=<id> opens and
highlights one batch order.
2026-09-27 12:45:50 +02:00

198 lines
7.3 KiB
Python

import logging
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.core.auth import RequirePermissionIfAuthEnabled, generate_api_key
from backend.app.core.database import get_db
from backend.app.core.permissions import Permission
from backend.app.models.api_key import APIKey
from backend.app.models.user import User
from backend.app.schemas.api_key import (
APIKeyCreate,
APIKeyCreateResponse,
APIKeyResponse,
APIKeyUpdate,
)
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),
_: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ),
):
"""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),
current_user: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_CREATE),
):
"""Create a new API key.
IMPORTANT: The full API key is only returned in this response.
Store it securely - it cannot be retrieved again.
"""
# Reject can_access_cloud on auth-disabled deployments — there's no per-user
# cloud_token to read against, so the flag would just silently do nothing.
# Surfacing the rejection at create time prevents the user from thinking
# they've configured cloud access when they actually haven't.
if data.can_access_cloud and current_user is None:
raise HTTPException(
status_code=400,
detail="can_access_cloud requires authentication to be enabled (per-user cloud tokens)",
)
# 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,
user_id=current_user.id if current_user else None,
can_queue=data.can_queue,
can_control_printer=data.can_control_printer,
can_read_status=data.can_read_status,
can_manage_library=data.can_manage_library,
can_manage_inventory=data.can_manage_inventory,
can_manage_maintenance=data.can_manage_maintenance,
can_manage_archives=data.can_manage_archives,
can_manage_projects=data.can_manage_projects,
can_access_cloud=data.can_access_cloud,
can_update_energy_cost=data.can_update_energy_cost,
can_send_notifications=data.can_send_notifications,
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
user_id=api_key.user_id,
can_queue=api_key.can_queue,
can_control_printer=api_key.can_control_printer,
can_read_status=api_key.can_read_status,
can_manage_library=api_key.can_manage_library,
can_manage_inventory=api_key.can_manage_inventory,
can_manage_maintenance=api_key.can_manage_maintenance,
can_manage_archives=api_key.can_manage_archives,
can_manage_projects=api_key.can_manage_projects,
can_access_cloud=api_key.can_access_cloud,
can_update_energy_cost=api_key.can_update_energy_cost,
can_send_notifications=api_key.can_send_notifications,
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),
_: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ),
):
"""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),
_: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_UPDATE),
):
"""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.can_manage_library is not None:
api_key.can_manage_library = data.can_manage_library
if data.can_manage_inventory is not None:
api_key.can_manage_inventory = data.can_manage_inventory
if data.can_manage_maintenance is not None:
api_key.can_manage_maintenance = data.can_manage_maintenance
if data.can_manage_archives is not None:
api_key.can_manage_archives = data.can_manage_archives
if data.can_manage_projects is not None:
api_key.can_manage_projects = data.can_manage_projects
if data.can_access_cloud is not None:
# Same constraint as create — flipping cloud access on a legacy key
# without an owner would be silently broken; reject at the route layer.
if data.can_access_cloud and api_key.user_id is None:
raise HTTPException(
status_code=400,
detail="can_access_cloud requires the API key to have an owner; recreate the key after upgrading",
)
api_key.can_access_cloud = data.can_access_cloud
if data.can_update_energy_cost is not None:
api_key.can_update_energy_cost = data.can_update_energy_cost
if data.can_send_notifications is not None:
api_key.can_send_notifications = data.can_send_notifications
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),
_: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_DELETE),
):
"""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"}