Files
wild-central/web/src/services/api/client.ts
Paul Payne 735f3fcb05 feat: Add Wild Central web app (Central-only UI)
Extract Central pages from the wild-cloud web app into a standalone
React app for Wild Central. Includes:

- Central overview, DNS, DHCP, Firewall, VPN, Ingress, CrowdSec pages
- Simplified sidebar with Central-only navigation
- Branding updated to "Wild Central"
- All Cloud-specific pages, components, hooks, and API services removed
- TypeScript type-check and production build pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:10:43 +00:00

143 lines
3.4 KiB
TypeScript

export class ApiError extends Error {
statusCode: number;
details?: unknown;
constructor(
message: string,
statusCode: number,
details?: unknown
) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.details = details;
}
}
interface ErrorResponseBody {
error?: string;
details?: unknown;
}
import { getApiBaseUrl } from './config';
export class ApiClient {
private baseUrl: string;
constructor(baseUrl: string = getApiBaseUrl()) {
this.baseUrl = baseUrl;
}
getBaseURL(): string {
return this.baseUrl;
}
private async request<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
const errorData: ErrorResponseBody = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
// Handle 204 No Content — intentional no-data for void operations
if (response.status === 204) {
return undefined as unknown as T;
}
return response.json();
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(
error instanceof Error ? error.message : 'Network error',
0
);
}
}
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' });
}
async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
});
}
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
async getText(endpoint: string): Promise<string> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
return response.text();
}
async putText(endpoint: string, text: string): Promise<{ message?: string; [key: string]: unknown }> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
body: text,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
return response.json();
}
}
export const apiClient = new ApiClient();