Files
local-mcp/static/js/api.js
cabbage 009fd039a2 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
2026-03-27 04:28:12 +08:00

79 lines
2.1 KiB
JavaScript

/**
* static/js/api.js
* Thin fetch wrappers for all HTTP API endpoints.
* Supports optional Bearer-token authentication.
*/
const BASE = '';
const TOKEN_KEY = 'local-mcp-token';
// Callback invoked when a 401 is received — set by app.js
let _onUnauthorized = null;
export function setUnauthorizedHandler(fn) {
_onUnauthorized = fn;
}
export function getStoredToken() {
return sessionStorage.getItem(TOKEN_KEY) || '';
}
export function setStoredToken(token) {
if (token) {
sessionStorage.setItem(TOKEN_KEY, token);
} else {
sessionStorage.removeItem(TOKEN_KEY);
}
}
async function request(method, path, body) {
const opts = {
method,
headers: { 'Content-Type': 'application/json' },
};
const token = getStoredToken();
if (token) {
opts.headers['Authorization'] = `Bearer ${token}`;
}
if (body !== undefined) opts.body = JSON.stringify(body);
const res = await fetch(BASE + path, opts);
if (res.status === 204) return null;
if (res.status === 401) {
if (_onUnauthorized) _onUnauthorized();
throw new Error('Unauthorized');
}
const data = await res.json();
if (!res.ok) {
const msg = data?.detail || `HTTP ${res.status}`;
throw new Error(msg);
}
return data;
}
export const api = {
// Health
health: () => request('GET', '/healthz'),
// Auth check (public — no token needed)
authCheck: () => fetch('/auth-check').then(r => r.json()),
// Status
status: () => request('GET', '/api/status'),
// Instructions
listInstructions: (status='all') => request('GET', `/api/instructions?status=${status}`),
createInstruction: (content) => request('POST', '/api/instructions', { content }),
updateInstruction: (id, content) => request('PATCH', `/api/instructions/${id}`, { content }),
deleteInstruction: (id) => request('DELETE', `/api/instructions/${id}`),
clearConsumed: () => request('DELETE', '/api/instructions/consumed'),
// Config
getConfig: () => request('GET', '/api/config'),
updateConfig: (patch) => request('PATCH', '/api/config', patch),
};