feat: optional Bearer-token authentication via API_TOKEN env var

Disabled by default (empty API_TOKEN). When set:
- All /api/* and /mcp requests require: Authorization: Bearer <token>
- Public exemptions: /, /healthz, /static/*, /auth-check
- Web UI: pre-flight /auth-check on load; shows token modal if required
- Token stored in sessionStorage, sent on every API request
- Mid-session 401s re-trigger the token modal
- MCP clients must pass the header: Authorization: Bearer <token>
Files changed:
- app/config.py: api_token field + API_TOKEN env var
- app/api/auth.py: Starlette BaseHTTPMiddleware for token enforcement
- main.py: register middleware + /auth-check public endpoint
- static/js/api.js: token storage, auth header, 401 handler hook
- static/js/app.js: auth pre-flight, showTokenModal(), bootstrap()
- static/css/components.css: .auth-overlay / .auth-card styles
- README.md: API_TOKEN env var docs + MCP client header example
This commit is contained in:
2026-03-27 04:28:12 +08:00
parent 1cc75afe87
commit 009fd039a2
7 changed files with 309 additions and 9 deletions

View File

@@ -16,6 +16,7 @@ from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from app.api import routes_config, routes_instructions, routes_status
from app.api.auth import TokenAuthMiddleware
from app.config import settings
from app.database import init_db
from app.logging_setup import configure_logging
@@ -60,6 +61,9 @@ def create_app() -> FastAPI:
lifespan=lifespan,
)
# --- Token auth middleware (no-op when API_TOKEN is not set) ---
app.add_middleware(TokenAuthMiddleware, token=settings.api_token)
# --- Global exception handler ---
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
@@ -69,6 +73,11 @@ def create_app() -> FastAPI:
content={"detail": "Internal server error", "error": str(exc)},
)
# --- Public: lets the UI know whether it needs a token (no auth required) ---
@app.get("/auth-check", include_in_schema=False)
def auth_check():
return {"auth_required": bool(settings.api_token)}
# --- API routers ---
app.include_router(routes_status.router)
app.include_router(routes_instructions.router)