Add multi-user automation features and per-user error tracking. - Database migrations: add workflow_configs/workflow_runs (004), app_errors (005), pipeline_configs/pipeline_runs (006), and add user_token_hash to app_errors (007). - Backend: introduce per-request token handling (X-API-Token) via app.api.deps and update many API routes (auth, automations, bank, characters, dashboard, events, exchange, logs) to use user-scoped Artifacts client and character scoping. Auth endpoints no longer store tokens server-side (validate-only); clear is a no-op on server. - New Errors API and services: endpoint to list, filter, resolve, and report errors scoped to the requesting user; add error models, schemas, middleware/error handler and error_service for recording/hashing tokens. - Pipelines & Workflows: add API routers, models, schemas and engine modules (pipeline/worker/coordinator, workflow runner/conditions) and action_executor updates to support workflow/pipeline execution. - Logs: logs endpoint now prefers fetching recent action logs from the game API (with fallback to local DB), supports paging and filtering, and scopes results to the user. - Frontend: add pipeline/workflow builders, lists, progress components and hooks (use-errors, use-pipelines, use-workflows), sentry client config, and updates to API client/constants/types. - Misc: add middleware error handler, various engine strategy tweaks, tests adjusted. Overall this change enables per-user API tokens, scopes DB queries to each user, introduces pipelines/workflows runtime support, and centralizes application error tracking.
97 lines
3.1 KiB
Python
97 lines
3.1 KiB
Python
"""Auth endpoints for per-user API token management.
|
|
|
|
Each user provides their own Artifacts API token via the frontend.
|
|
The token is stored in the browser's localStorage and sent with every
|
|
request as the ``X-API-Token`` header. The backend validates the token
|
|
but does NOT store it globally — this allows true multi-user support.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Request
|
|
from pydantic import BaseModel
|
|
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
|
|
|
|
class AuthStatus(BaseModel):
|
|
has_token: bool
|
|
source: str # "header", "env", or "none"
|
|
|
|
|
|
class SetTokenRequest(BaseModel):
|
|
token: str
|
|
|
|
|
|
class SetTokenResponse(BaseModel):
|
|
success: bool
|
|
source: str
|
|
account: str | None = None
|
|
error: str | None = None
|
|
|
|
|
|
@router.get("/status", response_model=AuthStatus)
|
|
async def auth_status(request: Request) -> AuthStatus:
|
|
"""Check whether the *requesting* client has a valid token.
|
|
|
|
The frontend sends the token in the ``X-API-Token`` header.
|
|
This endpoint tells the frontend whether that token is present.
|
|
"""
|
|
token = request.headers.get("X-API-Token")
|
|
if token:
|
|
return AuthStatus(has_token=True, source="header")
|
|
return AuthStatus(has_token=False, source="none")
|
|
|
|
|
|
@router.post("/token", response_model=SetTokenResponse)
|
|
async def validate_token(body: SetTokenRequest) -> SetTokenResponse:
|
|
"""Validate an Artifacts API token.
|
|
|
|
Does NOT store the token on the server. The frontend is responsible
|
|
for persisting it in localStorage and sending it with every request.
|
|
"""
|
|
token = body.token.strip()
|
|
if not token:
|
|
return SetTokenResponse(success=False, source="none", error="Token cannot be empty")
|
|
|
|
# Validate the token by making a test call
|
|
try:
|
|
async with httpx.AsyncClient(
|
|
base_url=settings.artifacts_api_url,
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/json",
|
|
},
|
|
timeout=httpx.Timeout(10.0),
|
|
) as test_client:
|
|
resp = await test_client.get("/my/characters")
|
|
if resp.status_code == 401:
|
|
return SetTokenResponse(success=False, source="none", error="Invalid token")
|
|
resp.raise_for_status()
|
|
except httpx.HTTPStatusError as exc:
|
|
return SetTokenResponse(
|
|
success=False,
|
|
source="none",
|
|
error=f"API error: {exc.response.status_code}",
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("Token validation failed: %s", exc)
|
|
return SetTokenResponse(
|
|
success=False,
|
|
source="none",
|
|
error="Could not validate token. Check your network connection.",
|
|
)
|
|
|
|
logger.info("API token validated via UI")
|
|
return SetTokenResponse(success=True, source="user")
|
|
|
|
|
|
@router.delete("/token")
|
|
async def clear_token() -> AuthStatus:
|
|
"""No-op on the backend — the frontend clears its own localStorage."""
|
|
return AuthStatus(has_token=False, source="none")
|