docuflow / src /lib /apiClient.ts
Joedroid's picture
feat: full admin dashboard with CRUD and env-based admin auth
2290d95
Raw
History Blame Contribute Delete
12 kB
// Centralized API client β€” routes all requests through Vite proxy (/api β†’ FastAPI 8000)
// Falls back to Express 3000 in production via API_BASE_URL or relative path
import { validateApiBaseUrl } from "../config";
const BASE = validateApiBaseUrl();
interface RequestOptions {
method?: string;
body?: any;
timeout?: number;
headers?: Record<string, string>;
}
class ApiError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.name = "ApiError";
this.status = status;
}
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { method = "GET", body, timeout = 30000, headers } = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const token = localStorage.getItem("workspace_auth_token");
const fetchOptions: RequestInit = {
method,
headers: {
"Content-Type": "application/json",
...(token ? { "Authorization": `Bearer ${token}` } : {}),
...(headers || {})
},
signal: controller.signal,
};
if (body && method !== "GET") {
fetchOptions.body = JSON.stringify(body);
}
const response = await fetch(`${BASE}${path}`, fetchOptions);
if (!response.ok) {
const errDetail = await response.text().catch(() => "");
throw new ApiError(
errDetail || `HTTP ${response.status}: ${response.statusText}`,
response.status
);
}
return (await response.json()) as T;
} finally {
clearTimeout(timer);
}
}
// ── AI Endpoints ──
export interface AIProcessResponse {
result: string;
provider: string;
model?: string;
tokensConsumed?: number;
}
export interface AIChatResponse {
result: string;
provider: string;
tokensConsumed?: number;
}
export function aiProcess(
text: string,
action: string,
targetLang?: string,
customOpenRouterKey?: string,
customModel?: string
): Promise<AIProcessResponse> {
return request<AIProcessResponse>("/api/ai/process", {
method: "POST",
body: { text, action, targetLang, customOpenRouterKey, customModel },
timeout: 60000,
});
}
export function aiChat(
documentContent: string,
history: { sender: string; text: string }[],
message: string,
customOpenRouterKey?: string,
customModel?: string
): Promise<AIChatResponse> {
return request<AIChatResponse>("/api/ai/chat", {
method: "POST",
body: { documentContent, history, message, customOpenRouterKey, customModel },
timeout: 60000,
});
}
// ── Sync Endpoints ──
export interface SyncResponse {
status: string;
timestamp: string;
newVersion: number;
content: string;
logs: string[];
activeCollaborators: number;
}
export function syncDocument(
docId: string,
content: string,
version: number,
teamChangesEnabled: boolean,
title?: string
): Promise<SyncResponse> {
return request<SyncResponse>("/api/sync", {
method: "POST",
body: { docId, content, version, teamChangesEnabled, title },
});
}
export interface ServerDocument {
id: string;
title: string;
content: string;
version: number;
lastSaved: string;
}
export function getDocuments(): Promise<{ documents: ServerDocument[] }> {
return request("/api/documents");
}
export function getDocument(docId: string): Promise<ServerDocument> {
return request<ServerDocument>(`/api/documents/${docId}`);
}
export function deleteDocument(docId: string): Promise<{ status: string }> {
return request(`/api/documents/${docId}`, { method: "DELETE" });
}
// ── Preferences Endpoints ──
export interface PreferencesSyncResponse {
status: string;
timestamp: string;
preferences: Record<string, any>;
}
export function syncPreferences(
preferences: Record<string, any>
): Promise<PreferencesSyncResponse> {
return request<PreferencesSyncResponse>("/api/preferences/sync", {
method: "POST",
body: { preferences },
});
}
export function getPreferences(): Promise<{ preferences: Record<string, any> }> {
return request("/api/preferences");
}
// ── Upload & Convert Endpoints ──
export interface ConvertResponse {
job_id: string;
status: string;
}
export interface ConvertStatusResponse {
job_id: string;
filename?: string;
status: string;
markdown?: string;
error?: string;
created_at?: string;
}
export async function uploadAndConvert(
file: File
): Promise<ConvertResponse> {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(`${BASE}/api/convert`, {
method: "POST",
body: formData,
});
if (!response.ok) {
const errText = await response.text().catch(() => "");
throw new ApiError(errText || `Upload failed: HTTP ${response.status}`, response.status);
}
return response.json();
}
export function getConvertStatus(jobId: string): Promise<ConvertStatusResponse> {
return request<ConvertStatusResponse>(`/api/convert/status/${jobId}`);
}
// ── Spellcheck Endpoint ──
export interface SpellcheckResponse {
misspelled: string[];
}
export function spellcheck(words: string[]): Promise<SpellcheckResponse> {
return request<SpellcheckResponse>("/api/spellcheck", {
method: "POST",
body: { words },
});
}
// ── Admin Endpoints ──
export interface AdminStatsResponse {
total_jobs: number;
completed_jobs: number;
failed_jobs: number;
pending_jobs: number;
total_users: number;
guest_users: number;
registered_users: number;
total_documents: number;
total_payments: number;
preferences_count: number;
system_uptime: string;
configs: Record<string, string>;
recent_jobs: any[];
}
export function adminVerify(password: string): Promise<{ status: string; token: string }> {
return request("/api/admin/verify", { method: "POST", body: { password } });
}
export function getAdminStats(token: string): Promise<AdminStatsResponse> {
return request("/api/admin/stats", { headers: { "Authorization": `Bearer ${token}` } });
}
export function setAdminConfig(configs: Record<string, string>, token: string): Promise<{ status: string; message: string }> {
return request("/api/admin/config", { method: "POST", body: { configs }, headers: { "Authorization": `Bearer ${token}` } });
}
export function purgeJobs(token: string): Promise<{ status: string; message: string }> {
return request("/api/admin/purge/jobs", { method: "POST", headers: { "Authorization": `Bearer ${token}` } });
}
export function purgeGuests(token: string): Promise<{ status: string; message: string }> {
return request("/api/admin/purge/guests", { method: "POST", headers: { "Authorization": `Bearer ${token}` } });
}
export function getAdminUsers(token: string, page: number = 1): Promise<{ total: number; users: any[] }> {
return request(`/api/admin/users?page=${page}`, { headers: { "Authorization": `Bearer ${token}` } });
}
export function updateAdminUser(userId: number, data: { tier?: string; display_name?: string }, token: string): Promise<{ status: string }> {
return request(`/api/admin/users/${userId}`, { method: "PATCH", body: data, headers: { "Authorization": `Bearer ${token}` } });
}
export function deleteAdminUser(userId: number, token: string): Promise<{ status: string }> {
return request(`/api/admin/users/${userId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` } });
}
export function getAdminDocuments(token: string, page: number = 1): Promise<{ total: number; documents: any[] }> {
return request(`/api/admin/documents?page=${page}`, { headers: { "Authorization": `Bearer ${token}` } });
}
export function deleteAdminDocument(docId: string, token: string): Promise<{ status: string }> {
return request(`/api/admin/documents/${docId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` } });
}
export function getAdminPayments(token: string, page: number = 1): Promise<{ total: number; payments: any[] }> {
return request(`/api/admin/payments?page=${page}`, { headers: { "Authorization": `Bearer ${token}` } });
}
export function getAdminAdmins(token: string): Promise<{ admins: any[] }> {
return request("/api/admin/admins", { headers: { "Authorization": `Bearer ${token}` } });
}
export function addAdmin(email: string, token: string): Promise<{ status: string; message: string }> {
return request("/api/admin/admins/add", { method: "POST", body: { email }, headers: { "Authorization": `Bearer ${token}` } });
}
export function removeAdmin(userId: number, token: string): Promise<{ status: string }> {
return request(`/api/admin/admins/${userId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}` } });
}
export function getApiDocs(): Promise<any> {
return request("/api/docs");
}
// ── Billing & Payment Endpoints ──
export interface BillingConfig {
active_provider: string;
supported_providers: string[];
}
export interface CheckoutResponse {
status: string;
url: string;
provider: string;
}
export interface PaymentStatusResponse {
status: string;
tier: string;
provider?: string;
timestamp?: string;
}
export function getBillingConfig(): Promise<BillingConfig> {
return request<BillingConfig>("/api/billing/config");
}
export function createCheckoutSession(
email: string,
amount: number,
provider?: string,
successUrl?: string,
cancelUrl?: string
): Promise<CheckoutResponse> {
return request<CheckoutResponse>("/api/billing/create-checkout-session", {
method: "POST",
body: { email, amount, provider, success_url: successUrl, cancel_url: cancelUrl }
});
}
export function getPaymentStatus(email: string): Promise<PaymentStatusResponse> {
return request<PaymentStatusResponse>(`/api/billing/status/${email}`);
}
// ── Authentication Endpoints ──
export interface AuthResponse {
status: string;
token: string;
user: {
id: number;
email: string | null;
display_name: string;
is_guest: boolean;
tier: "free" | "pro" | "enterprise";
has_passkey: boolean;
oauth_provider: string | null;
};
}
// Passkey Registration
export function passkeyRegisterStart(displayName: string): Promise<{ options: any; challenge_key: string }> {
return request("/api/auth/passkey/register/start", {
method: "POST",
body: { display_name: displayName }
});
}
export function passkeyRegisterFinish(challengeKey: string, credential: any): Promise<AuthResponse> {
return request("/api/auth/passkey/register/finish", {
method: "POST",
body: { challenge_key: challengeKey, credential }
});
}
// Passkey Login
export function passkeyLoginStart(): Promise<{ options: any; challenge_key: string }> {
return request("/api/auth/passkey/login/start", { method: "POST" });
}
export function passkeyLoginFinish(challengeKey: string, credential: any): Promise<AuthResponse> {
return request("/api/auth/passkey/login/finish", {
method: "POST",
body: { challenge_key: challengeKey, credential }
});
}
// OAuth
export function oauthStart(provider: string, redirectUri: string): Promise<{ url: string; provider: string }> {
return request("/api/auth/oauth/start", {
method: "POST",
body: { provider, redirect_uri: redirectUri }
});
}
export function oauthCallback(provider: string, code: string, redirectUri: string): Promise<AuthResponse> {
return request("/api/auth/oauth/callback", {
method: "POST",
body: { provider, code, redirect_uri: redirectUri }
});
}
// Guest (backward compat)
export function authGuest(deviceUuid: string): Promise<AuthResponse> {
return request<AuthResponse>("/api/auth/guest", {
method: "POST",
body: { device_uuid: deviceUuid }
});
}
export function getMe(token: string): Promise<AuthResponse> {
return request<AuthResponse>("/api/auth/me", {
headers: { "Authorization": `Bearer ${token}` }
});
}
export { ApiError };