// API client for stable-diffusion REST API // Type for server config injected by the server declare global { interface Window { __SERVER_CONFIG__?: { apiUrl: string; apiBasePath: string; host: string; port: number; authMethod: "none" | "unix" | "jwt"; authEnabled: boolean; }; } } // Request throttling to prevent excessive API calls class RequestThrottler { private requests: Map = new Map(); private maxRequests: number = 10; // Max requests per time window private timeWindow: number = 1000; // Time window in milliseconds canMakeRequest(key: string): boolean { const now = Date.now(); const request = this.requests.get(key); if (!request || now >= request.resetTime) { this.requests.set(key, { count: 1, resetTime: now + this.timeWindow }); return true; } if (request.count >= this.maxRequests) { return false; } request.count++; return true; } getWaitTime(key: string): number { const request = this.requests.get(key); if (!request) return 0; const now = Date.now(); if (now >= request.resetTime) return 0; return request.resetTime - now; } } // Global throttler instance const throttler = new RequestThrottler(); // Debounce utility for frequent calls function _debounce unknown>( func: T, wait: number, immediate?: boolean, ): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; return function executedFunction(...args: Parameters) { const later = () => { timeout = null; if (!immediate) func(...args); }; const callNow = immediate && !timeout; if (timeout) clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func(...args); }; } // Cache for API responses to reduce redundant calls class ApiCache { private cache: Map = new Map(); private defaultTtl: number = 5000; // 5 seconds default TTL set(key: string, data: unknown, ttl?: number): void { this.cache.set(key, { data, timestamp: Date.now(), ttl: ttl || this.defaultTtl, }); } get(key: string): unknown | null { const cached = this.cache.get(key); if (!cached) return null; if (Date.now() - cached.timestamp > cached.ttl) { this.cache.delete(key); return null; } return cached.data; } clear(): void { this.cache.clear(); } delete(key: string): void { this.cache.delete(key); } } const cache = new ApiCache(); // Get configuration from server-injected config or fallback to environment/defaults // This function is called at runtime to ensure __SERVER_CONFIG__ is available function getApiConfig() { if (typeof window !== "undefined" && window.__SERVER_CONFIG__) { let apiUrl = window.__SERVER_CONFIG__.apiUrl; // Fix 0.0.0.0 host to use actual browser host if (apiUrl && apiUrl.includes("0.0.0.0")) { const protocol = window.location.protocol; const host = window.location.hostname; const port = window.location.port; apiUrl = `${protocol}//${host}${port ? ":" + port : ""}`; } return { apiUrl, apiBase: window.__SERVER_CONFIG__.apiBasePath, }; } // Fallback for development mode - use current window location if (typeof window !== "undefined") { const protocol = window.location.protocol; const host = window.location.hostname; const port = window.location.port; return { apiUrl: `${protocol}//${host}:${port}`, apiBase: "/api", }; } // Server-side fallback return { apiUrl: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8081", apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH || "/api", }; } export interface GenerationRequest { model?: string; prompt: string; negative_prompt?: string; width?: number; height?: number; steps?: number; cfg_scale?: number; seed?: string; sampling_method?: string; scheduler?: string; batch_count?: number; clip_skip?: number; strength?: number; control_strength?: number; } export interface JobInfo { id?: string; request_id?: string; status: | "pending" | "loading" | "processing" | "completed" | "failed" | "cancelled" | "queued"; progress?: number; model_load_progress?: number; generation_progress?: number; overall_progress?: number; result?: { images: string[]; }; outputs?: Array<{ filename: string; url: string; path: string; }>; error?: string; created_at?: string; updated_at?: string; message?: string; queue_position?: number; prompt?: string; end_time?: number; start_time?: number; queued_time?: number; error_message?: string; position?: number; } // API response wrapper for job details export interface JobDetailsResponse { job: JobInfo; } export interface ModelInfo { id?: string; name: string; path?: string; type: string; size?: number; file_size?: number; file_size_mb?: number; sha256?: string | null; sha256_short?: string | null; loaded?: boolean; architecture?: string; required_models?: RequiredModelInfo[]; recommended_vae?: RecommendedModelInfo; recommended_textual_inversions?: RecommendedModelInfo[]; recommended_loras?: RecommendedModelInfo[]; metadata?: Record; } export interface RequiredModelInfo { type: string; name?: string; description?: string; optional?: boolean; priority?: number; } export interface RecommendedModelInfo { type: string; name?: string; description?: string; reason?: string; } export interface AutoSelectionState { selectedModels: Record; // modelType -> modelName autoSelectedModels: Record; // modelType -> modelName missingModels: string[]; // modelType names warnings: string[]; errors: string[]; isAutoSelecting: boolean; } export interface EnhancedModelsResponse { models: ModelInfo[]; pagination: { page: number; limit: number; total_count: number; total_pages: number; has_next: boolean; has_prev: boolean; }; statistics: Record; auto_selection?: AutoSelectionState; } export interface QueueStatus { active_generations: number; jobs: JobInfo[]; running: boolean; size: number; } export interface HealthStatus { status: "ok" | "error" | "degraded"; message: string; timestamp: string; uptime?: number; version?: string; } export interface VersionInfo { version: string; type: string; commit: { short: string; full: string; }; branch: string; clean: boolean; build_time: string; } class ApiClient { private baseUrl: string = ""; private isInitialized: boolean = false; // Initialize base URL private initBaseUrl(): string { if (!this.isInitialized) { const config = getApiConfig(); this.baseUrl = `${config.apiUrl}${config.apiBase}`; this.isInitialized = true; } return this.baseUrl; } // Get base URL dynamically at runtime to ensure server config is loaded private getBaseUrl(): string { return this.initBaseUrl(); } private async request( endpoint: string, options: RequestInit = {}, ): Promise { const url = `${this.getBaseUrl()}${endpoint}`; // Check request throttling for certain endpoints const needsThrottling = endpoint.includes("/queue/status") || endpoint.includes("/health"); if (needsThrottling) { const waitTime = throttler.getWaitTime(endpoint); if (waitTime > 0) { // Wait before making the request await new Promise((resolve) => setTimeout(resolve, waitTime)); } if (!throttler.canMakeRequest(endpoint)) { throw new Error( "Too many requests. Please wait before making another request.", ); } } // Get authentication method from server config const authMethod = typeof window !== "undefined" && window.__SERVER_CONFIG__ ? window.__SERVER_CONFIG__.authMethod : "jwt"; // Add auth token or Unix user header based on auth method const token = typeof window !== "undefined" ? localStorage.getItem("auth_token") : null; const unixUser = typeof window !== "undefined" ? localStorage.getItem("unix_user") : null; const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; if (authMethod === "unix" && unixUser) { // For Unix auth, send username in X-Unix-User header headers["X-Unix-User"] = unixUser; } else if (token) { // For JWT auth, send the token in Authorization header headers["Authorization"] = `Bearer ${token}`; } const response = await fetch(url, { ...options, headers, }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: { message: response.statusText }, })); // Handle nested error structure: { error: { message: "..." } } const errorMessage = errorData.error?.message || errorData.message || errorData.error || "API request failed"; throw new Error(errorMessage); } return response.json(); } // Enhanced health check with caching and better error handling async checkHealth(): Promise { const cacheKey = "health_check"; const cachedResult = cache.get(cacheKey) as HealthStatus | null; if (cachedResult) { return cachedResult; } const endpoints = ["/queue/status", "/health", "/status", "/"]; for (const endpoint of endpoints) { try { const response = await fetch(`${this.getBaseUrl()}${endpoint}`, { method: "GET", headers: { "Content-Type": "application/json", }, // Add timeout to prevent hanging requests signal: AbortSignal.timeout(3000), // Reduced timeout }); if (response.ok) { const data = await response.json(); // For queue status, consider it healthy if it returns valid structure if (endpoint === "/queue/status" && data.queue) { const result = { status: "ok" as const, message: "API is running and queue is accessible", timestamp: new Date().toISOString(), }; cache.set(cacheKey, result, 10000); // Cache for 10 seconds return result; } // For other health endpoints const healthStatus: HealthStatus = { status: "ok", message: "API is running", timestamp: new Date().toISOString(), uptime: data.uptime, version: data.version || data.build || data.git_version, }; // Handle different response formats if (data.status) { if ( data.status === "healthy" || data.status === "running" || data.status === "ok" ) { healthStatus.status = "ok"; healthStatus.message = data.message || "API is running"; } else if (data.status === "degraded") { healthStatus.status = "degraded"; healthStatus.message = data.message || "API is running in degraded mode"; } else { healthStatus.status = "error"; healthStatus.message = data.message || "API status is unknown"; } } cache.set(cacheKey, healthStatus, 10000); // Cache for 10 seconds return healthStatus; } } catch (error) { // Continue to next endpoint if this one fails console.warn(`Health check failed for endpoint ${endpoint}:`, error); continue; } } // If all endpoints fail throw new Error("All health check endpoints are unavailable"); } // Alternative simple connectivity check with caching async checkConnectivity(): Promise { const cacheKey = "connectivity_check"; const cachedResult = cache.get(cacheKey) as boolean | undefined; if (cachedResult !== undefined) { return cachedResult; } try { const response = await fetch(`${this.getBaseUrl()}`, { method: "HEAD", signal: AbortSignal.timeout(2000), // Reduced timeout }); const result = response.ok || response.status < 500; cache.set(cacheKey, result, 5000); // Cache for 5 seconds return result; } catch { cache.set(cacheKey, false, 5000); // Cache failure for 5 seconds return false; } } // Generation endpoints async generateImage(params: GenerationRequest): Promise { return this.request("/generate/text2img", { method: "POST", body: JSON.stringify(params), }); } async text2img(params: GenerationRequest): Promise { return this.request("/generate/text2img", { method: "POST", body: JSON.stringify(params), }); } async img2img( params: GenerationRequest & { image: string }, ): Promise { // Convert frontend field name to backend field name const backendParams = { ...params, init_image: params.image, image: undefined, // Remove the frontend field }; return this.request("/generate/img2img", { method: "POST", body: JSON.stringify(backendParams), }); } async inpainting( params: GenerationRequest & { source_image: string; mask_image: string }, ): Promise { return this.request("/generate/inpainting", { method: "POST", body: JSON.stringify(params), }); } async upscale( params: { image: string; model: string; upscale_factor: number; } ): Promise { // Map 'model' to 'esrgan_model' as expected by the API const apiParams = { image: params.image, esrgan_model: params.model, upscale_factor: params.upscale_factor, }; return this.request("/generate/upscale", { method: "POST", body: JSON.stringify(apiParams), }); } // Job management with caching for status checks async getJobStatus(jobId: string): Promise { const cacheKey = `job_status_${jobId}`; const cachedResult = cache.get(cacheKey) as JobDetailsResponse | null; if (cachedResult) { return cachedResult; } const result = await this.request( `/queue/job/${jobId}`, ); // Cache job status for a short time if (result.job.status === "processing" || result.job.status === "queued") { cache.set(cacheKey, result, 2000); // Cache for 2 seconds for active jobs } else { cache.set(cacheKey, result, 10000); // Cache for 10 seconds for completed jobs } return result; } // Get authenticated image URL with cache-busting getImageUrl(jobId: string, filename: string): string { const baseUrl = this.getBaseUrl(); // Add cache-busting timestamp const timestamp = Date.now(); const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`; return url; } // Download image with authentication async downloadImage(jobId: string, filename: string): Promise { const url = this.getImageUrl(jobId, filename); // Get authentication method from server config const authMethod = typeof window !== "undefined" && window.__SERVER_CONFIG__ ? window.__SERVER_CONFIG__.authMethod : "jwt"; // Add auth token or Unix user header based on auth method const token = typeof window !== "undefined" ? localStorage.getItem("auth_token") : null; const unixUser = typeof window !== "undefined" ? localStorage.getItem("unix_user") : null; const headers: Record = {}; if (authMethod === "unix" && unixUser) { // For Unix auth, send the username in X-Unix-User header headers["X-Unix-User"] = unixUser; } else if (token) { // For JWT auth, send the token in Authorization header headers["Authorization"] = `Bearer ${token}`; } const response = await fetch(url, { headers, }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: { message: response.statusText }, })); // Handle nested error structure: { error: { message: "..." } } const errorMessage = errorData.error?.message || errorData.message || errorData.error || "Failed to download image"; throw new Error(errorMessage); } return response.blob(); } // Download image from URL with server-side proxy to avoid CORS issues async downloadImageFromUrl(url: string): Promise<{ mimeType: string; filename: string; base64Data: string; tempUrl?: string; tempFilename?: string; }> { const apiUrl = `${this.getBaseUrl()}/image/download?url=${encodeURIComponent(url)}`; const response = await fetch(apiUrl); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: { message: response.statusText }, })); // Handle nested error structure: { error: { message: "..." } } const errorMessage = errorData.error?.message || errorData.message || errorData.error || "Failed to download image from URL"; throw new Error(errorMessage); } const result = await response.json(); return { mimeType: result.mime_type, filename: result.filename, base64Data: result.base64_data, tempUrl: result.temp_url, tempFilename: result.temp_filename, }; } async cancelJob(jobId: string): Promise { // Clear job status cache when cancelling cache.delete(`job_status_${jobId}`); return this.request("/queue/cancel", { method: "POST", body: JSON.stringify({ job_id: jobId }), }); } // Get queue status with caching and throttling async getQueueStatus(): Promise { const cacheKey = "queue_status"; const cachedResult = cache.get(cacheKey) as QueueStatus | null; if (cachedResult) { return cachedResult; } const response = await this.request<{ queue: QueueStatus }>( "/queue/status", ); // Cache queue status based on current activity const hasActiveJobs = response.queue.jobs.some( (job) => job.status === "processing" || job.status === "queued", ); // Cache for shorter time if there are active jobs const cacheTime = hasActiveJobs ? 1000 : 5000; // 1 second for active, 5 seconds for idle cache.set(cacheKey, response.queue, cacheTime); return response.queue; } async clearQueue(): Promise { // Clear all related caches cache.delete("queue_status"); return this.request("/queue/clear", { method: "POST", }); } // Model management async getModels( type?: string, loaded?: boolean, page: number = 1, limit: number = -1, search?: string, ): Promise { const cacheKey = `models_${type || "all"}_${loaded ? "loaded" : "all"}_${page}_${limit}_${search || "all"}`; const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null; if (cachedResult) { return cachedResult; } let endpoint = "/models"; const params = []; if (type && type !== "loaded") params.push(`type=${type}`); if (type === "loaded" || loaded) params.push("loaded=true"); // Only add page parameter if we're using pagination (limit > 0) if (limit > 0) { params.push(`page=${page}`); params.push(`limit=${limit}`); } else { // When limit is 0 (default), we want all models, so add limit=0 to disable pagination params.push("limit=0"); } if (search) params.push(`search=${encodeURIComponent(search)}`); // Add include_metadata for additional information params.push("include_metadata=true"); if (params.length > 0) endpoint += "?" + params.join("&"); const response = await this.request(endpoint); const models = response.models.map((model) => ({ ...model, id: model.sha256_short || model.sha256 || model.id || model.name, size: model.file_size || model.size, path: model.path || model.name, })); const result = { ...response, models, }; // Cache models for 30 seconds as they don't change frequently cache.set(cacheKey, result, 30000); return result; } // Get models with automatic selection information async getModelsForAutoSelection( checkpointModel?: string, ): Promise { const cacheKey = `models_auto_selection_${checkpointModel || "none"}`; const cachedResult = cache.get(cacheKey) as EnhancedModelsResponse | null; if (cachedResult) { return cachedResult; } let endpoint = "/models"; const params = []; params.push("include_metadata=true"); params.push("include_requirements=true"); if (checkpointModel) { params.push(`checkpoint=${encodeURIComponent(checkpointModel)}`); } params.push("limit=0"); // Get all models if (params.length > 0) endpoint += "?" + params.join("&"); const response = await this.request(endpoint); const models = response.models.map((model) => ({ ...model, id: model.sha256_short || model.sha256 || model.id || model.name, size: model.file_size || model.size, path: model.path || model.name, })); const result = { ...response, models, }; // Cache for 30 seconds cache.set(cacheKey, result, 30000); return result; } // Utility function to get models by type getModelsByType(models: ModelInfo[], type: string): ModelInfo[] { return models.filter( (model) => model.type.toLowerCase() === type.toLowerCase(), ); } // Utility function to find models by name pattern findModelsByName(models: ModelInfo[], namePattern: string): ModelInfo[] { const pattern = namePattern.toLowerCase(); return models.filter((model) => model.name.toLowerCase().includes(pattern)); } // Utility function to get loaded models by type getLoadedModelsByType(models: ModelInfo[], type: string): ModelInfo[] { return this.getModelsByType(models, type).filter((model) => model.loaded); } // Get all models (for backward compatibility) async getAllModels(type?: string, loaded?: boolean): Promise { const allModels: ModelInfo[] = []; let page = 1; const limit = 100; while (true) { const response = await this.getModels(type, loaded, page, limit); allModels.push(...response.models); if (!response.pagination.has_next) { break; } page++; } return allModels; } async getModelInfo(modelId: string): Promise { const cacheKey = `model_info_${modelId}`; const cachedResult = cache.get(cacheKey) as ModelInfo | null; if (cachedResult) { return cachedResult; } const result = await this.request(`/models/${modelId}`); cache.set(cacheKey, result, 30000); // Cache for 30 seconds return result; } async loadModel(modelId: string): Promise { // Clear model cache when loading cache.delete(`model_info_${modelId}`); try { return await this.request(`/models/${modelId}/load`, { method: "POST", }); } catch (error) { // Enhance error message for hash validation failures const errorMessage = error instanceof Error ? error.message : "Failed to load model"; if (errorMessage.includes("INVALID_MODEL_IDENTIFIER") || errorMessage.includes("MODEL_NOT_FOUND")) { throw new Error(`Hash validation failed: ${errorMessage}. Please ensure you're using a valid model hash instead of a name.`); } throw error; } } async unloadModel(modelId: string): Promise { // Clear model cache when unloading cache.delete(`models_*`); try { return await this.request(`/models/${modelId}/unload`, { method: "POST", }); } catch (error) { // Enhance error message for hash validation failures const errorMessage = error instanceof Error ? error.message : "Failed to unload model"; if (errorMessage.includes("INVALID_MODEL_IDENTIFIER") || errorMessage.includes("MODEL_NOT_FOUND")) { throw new Error(`Hash validation failed: ${errorMessage}. Please ensure you're using a valid model hash instead of a name.`); } throw error; } } async computeModelHash(modelId: string): Promise<{request_id: string, status: string, message: string}> { // Clear model cache when computing hash cache.delete(`models_*`); return this.request<{request_id: string, status: string, message: string}>(`/models/hash`, { method: "POST", body: JSON.stringify({ models: [modelId] }), }); } async scanModels(): Promise { // Clear all model caches when scanning cache.clear(); return this.request("/models/refresh", { method: "POST", }); } async getModelTypes(): Promise< Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string; }> > { const cacheKey = "model_types"; const cachedResult = cache.get(cacheKey) as Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string; }> | null; if (cachedResult) { return cachedResult; } const response = await this.request<{ model_types: Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string; }>; }>("/models/types"); cache.set(cacheKey, response.model_types, 60000); // Cache for 1 minute return response.model_types; } async convertModel( modelName: string, quantizationType: string, outputPath?: string, ): Promise<{ request_id: string; message: string }> { return this.request<{ request_id: string; message: string }>( "/models/convert", { method: "POST", body: JSON.stringify({ model_name: modelName, quantization_type: quantizationType, output_path: outputPath, }), }, ); } // System endpoints async getHealth(): Promise<{ status: string }> { return this.request<{ status: string }>("/health"); } async getStatus(): Promise> { return this.request>("/status"); } async getSystemInfo(): Promise> { return this.request>("/system"); } async restartServer(): Promise<{ message: string }> { return this.request<{ message: string }>("/system/restart", { method: "POST", body: JSON.stringify({}), }); } // Image manipulation endpoints async resizeImage( image: string, width: number, height: number, ): Promise<{ image: string }> { return this.request<{ image: string }>("/image/resize", { method: "POST", body: JSON.stringify({ image, width, height, }), }); } async cropImage( image: string, x: number, y: number, width: number, height: number, ): Promise<{ image: string }> { return this.request<{ image: string }>("/image/crop", { method: "POST", body: JSON.stringify({ image, x, y, width, height, }), }); } // Configuration endpoints with caching async getSamplers(): Promise< Array<{ name: string; description: string; recommended_steps: number }> > { const cacheKey = "samplers"; const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string; recommended_steps: number }> | null; if (cachedResult) { return cachedResult; } const response = await this.request<{ samplers: Array<{ name: string; description: string; recommended_steps: number; }>; }>("/samplers"); cache.set(cacheKey, response.samplers, 60000); // Cache for 1 minute return response.samplers; } async getSchedulers(): Promise> { const cacheKey = "schedulers"; const cachedResult = cache.get(cacheKey) as Array<{ name: string; description: string }> | null; if (cachedResult) { return cachedResult; } const response = await this.request<{ schedulers: Array<{ name: string; description: string }>; }>("/schedulers"); cache.set(cacheKey, response.schedulers, 60000); // Cache for 1 minute return response.schedulers; } // Cache management methods clearCache(): void { cache.clear(); } clearCacheByPrefix(prefix: string): void { const keysToDelete: string[] = []; // Access the private cache property through type assertion for cleanup const cacheInstance = cache as unknown as { cache: Map }; cacheInstance.cache.forEach((_: unknown, key: string) => { if (key.startsWith(prefix)) { keysToDelete.push(key); } }); keysToDelete.forEach((key) => cache.delete(key)); } } // Generic API request function for authentication export async function apiRequest( endpoint: string, options: RequestInit = {}, ): Promise { const { apiUrl, apiBase } = getApiConfig(); const url = `${apiUrl}${apiBase}${endpoint}`; // For both Unix and JWT auth, send username and password // The server will handle whether password is required based on PAM availability // Get authentication method from server config const authMethod = typeof window !== "undefined" && window.__SERVER_CONFIG__ ? window.__SERVER_CONFIG__.authMethod : "jwt"; // Add auth token or Unix user header based on auth method const token = typeof window !== "undefined" ? localStorage.getItem("auth_token") : null; const unixUser = typeof window !== "undefined" ? localStorage.getItem("unix_user") : null; const headers: Record = { "Content-Type": "application/json", ...(options.headers as Record), }; if (authMethod === "unix" && unixUser) { // For Unix auth, send the username in X-Unix-User header headers["X-Unix-User"] = unixUser; } else if (token) { // For JWT auth, send the token in Authorization header headers["Authorization"] = `Bearer ${token}`; } return fetch(url, { ...options, headers, }); } // Authentication API endpoints export const authApi = { async login(username: string, password?: string) { // Get authentication method from server config const authMethod = typeof window !== "undefined" && window.__SERVER_CONFIG__ ? window.__SERVER_CONFIG__.authMethod : "jwt"; // For both Unix and JWT auth, send username and password // The server will handle whether password is required based on PAM availability const response = await apiRequest("/auth/login", { method: "POST", body: JSON.stringify({ username, password }), }); if (!response.ok) { const error = await response .json() .catch(() => ({ message: "Login failed" })); throw new Error(error.message || "Login failed"); } return response.json(); }, async validateToken(token: string) { const response = await apiRequest("/auth/validate", { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) { throw new Error("Token validation failed"); } return response.json(); }, async refreshToken() { const response = await apiRequest("/auth/refresh", { method: "POST", }); if (!response.ok) { throw new Error("Token refresh failed"); } return response.json(); }, async logout() { await apiRequest("/auth/logout", { method: "POST", }); }, async getCurrentUser() { const response = await apiRequest("/auth/me"); if (!response.ok) { throw new Error("Failed to get current user"); } return response.json(); }, async changePassword(oldPassword: string, newPassword: string) { const response = await apiRequest("/auth/change-password", { method: "POST", body: JSON.stringify({ old_password: oldPassword, new_password: newPassword, }), }); if (!response.ok) { const error = await response .json() .catch(() => ({ message: "Password change failed" })); throw new Error(error.message || "Password change failed"); } return response.json(); }, // API Key management async getApiKeys() { const response = await apiRequest("/auth/api-keys"); if (!response.ok) { throw new Error("Failed to get API keys"); } return response.json(); }, async createApiKey(name: string, scopes?: string[]) { const response = await apiRequest("/auth/api-keys", { method: "POST", body: JSON.stringify({ name, scopes }), }); if (!response.ok) { const error = await response .json() .catch(() => ({ message: "Failed to create API key" })); throw new Error(error.message || "Failed to create API key"); } return response.json(); }, async deleteApiKey(keyId: string) { const response = await apiRequest(`/auth/api-keys/${keyId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Failed to delete API key"); } return response.json(); }, // User management (admin only) async getUsers() { const response = await apiRequest("/auth/users"); if (!response.ok) { throw new Error("Failed to get users"); } return response.json(); }, async createUser(userData: { username: string; email?: string; password: string; role?: string; }) { const response = await apiRequest("/auth/users", { method: "POST", body: JSON.stringify(userData), }); if (!response.ok) { const error = await response .json() .catch(() => ({ message: "Failed to create user" })); throw new Error(error.message || "Failed to create user"); } return response.json(); }, async updateUser( userId: string, userData: { email?: string; role?: string; active?: boolean }, ) { const response = await apiRequest(`/auth/users/${userId}`, { method: "PUT", body: JSON.stringify(userData), }); if (!response.ok) { const error = await response .json() .catch(() => ({ message: "Failed to update user" })); throw new Error(error.message || "Failed to update user"); } return response.json(); }, async deleteUser(userId: string) { const response = await apiRequest(`/auth/users/${userId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Failed to delete user"); } return response.json(); }, }; // Version API export async function getVersion(): Promise { const response = await apiRequest("/version"); if (!response.ok) { throw new Error("Failed to get version information"); } return response.json(); } export const apiClient = new ApiClient();