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

@@ -480,3 +480,83 @@
to { transform: rotate(360deg); }
}
/* ── Auth overlay ─────────────────────────────────────────────────────────── */
.auth-overlay {
position: fixed;
inset: 0;
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.auth-card {
width: 100%;
max-width: 380px;
margin: var(--space-4);
padding: var(--space-8);
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.48);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
text-align: center;
}
.auth-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background: color-mix(in srgb, var(--cyan) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--cyan) 25%, transparent);
margin-bottom: var(--space-2);
}
.auth-card__title {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.auth-card__desc {
font-size: var(--text-sm);
color: var(--text-muted);
margin: 0;
line-height: 1.6;
}
.auth-card__desc code {
font-family: var(--font-mono);
font-size: 0.9em;
color: var(--cyan);
background: color-mix(in srgb, var(--cyan) 10%, transparent);
padding: 1px 5px;
border-radius: 3px;
}
.auth-card__form {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.auth-card__error {
font-size: var(--text-xs);
color: var(--red);
margin: 0;
}