Add bearer token API authentication
- Auto-generate random 32-char bearer token on first startup, stored in secrets.yaml as api.bearerToken - BearerAuthMiddleware checks Authorization: Bearer <token> on all /api/ endpoints except /health, /health/reconcile, /events (SSE), and non-API paths (frontend static files) - Development mode (WILD_CENTRAL_ENV=development) skips auth entirely - Web app ApiClient: add setToken/clearToken/hasToken methods, persist token in localStorage, automatically include Authorization header on all API requests - Token can be found in secrets.yaml for CLI/automation use
This commit is contained in:
@@ -21,17 +21,43 @@ interface ErrorResponseBody {
|
||||
|
||||
import { getApiBaseUrl } from './config';
|
||||
|
||||
const TOKEN_KEY = 'wild-central:token';
|
||||
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
private token: string | null = null;
|
||||
|
||||
constructor(baseUrl: string = getApiBaseUrl()) {
|
||||
this.baseUrl = baseUrl;
|
||||
// Load token from localStorage on init
|
||||
try { this.token = localStorage.getItem(TOKEN_KEY); } catch { /* noop */ }
|
||||
}
|
||||
|
||||
getBaseURL(): string {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
try { localStorage.setItem(TOKEN_KEY, token); } catch { /* noop */ }
|
||||
}
|
||||
|
||||
clearToken() {
|
||||
this.token = null;
|
||||
try { localStorage.removeItem(TOKEN_KEY); } catch { /* noop */ }
|
||||
}
|
||||
|
||||
hasToken(): boolean {
|
||||
return this.token !== null && this.token !== '';
|
||||
}
|
||||
|
||||
private authHeaders(): Record<string, string> {
|
||||
if (this.token) {
|
||||
return { 'Authorization': `Bearer ${this.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
@@ -43,6 +69,7 @@ export class ApiClient {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.authHeaders(),
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
@@ -104,7 +131,7 @@ export class ApiClient {
|
||||
|
||||
async getText(endpoint: string): Promise<string> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url, { headers: this.authHeaders() });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
@@ -122,7 +149,7 @@ export class ApiClient {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
headers: { 'Content-Type': 'text/plain', ...this.authHeaders() },
|
||||
body: text,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user