api.ts 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. // API client for stable-diffusion REST API
  2. // Type for server config injected by the server
  3. declare global {
  4. interface Window {
  5. __SERVER_CONFIG__?: {
  6. apiUrl: string;
  7. apiBasePath: string;
  8. host: string;
  9. port: number;
  10. authMethod: 'none' | 'unix' | 'jwt';
  11. authEnabled: boolean;
  12. };
  13. }
  14. }
  15. // Request throttling to prevent excessive API calls
  16. class RequestThrottler {
  17. private requests: Map<string, { count: number; resetTime: number }> = new Map();
  18. private maxRequests: number = 10; // Max requests per time window
  19. private timeWindow: number = 1000; // Time window in milliseconds
  20. canMakeRequest(key: string): boolean {
  21. const now = Date.now();
  22. const request = this.requests.get(key);
  23. if (!request || now >= request.resetTime) {
  24. this.requests.set(key, { count: 1, resetTime: now + this.timeWindow });
  25. return true;
  26. }
  27. if (request.count >= this.maxRequests) {
  28. return false;
  29. }
  30. request.count++;
  31. return true;
  32. }
  33. getWaitTime(key: string): number {
  34. const request = this.requests.get(key);
  35. if (!request) return 0;
  36. const now = Date.now();
  37. if (now >= request.resetTime) return 0;
  38. return request.resetTime - now;
  39. }
  40. }
  41. // Global throttler instance
  42. const throttler = new RequestThrottler();
  43. // Debounce utility for frequent calls
  44. function debounce<T extends (...args: any[]) => any>(
  45. func: T,
  46. wait: number,
  47. immediate?: boolean
  48. ): (...args: Parameters<T>) => void {
  49. let timeout: NodeJS.Timeout | null = null;
  50. return function executedFunction(...args: Parameters<T>) {
  51. const later = () => {
  52. timeout = null;
  53. if (!immediate) func(...args);
  54. };
  55. const callNow = immediate && !timeout;
  56. if (timeout) clearTimeout(timeout);
  57. timeout = setTimeout(later, wait);
  58. if (callNow) func(...args);
  59. };
  60. }
  61. // Cache for API responses to reduce redundant calls
  62. class ApiCache {
  63. private cache: Map<string, { data: any; timestamp: number; ttl: number }> = new Map();
  64. private defaultTtl: number = 5000; // 5 seconds default TTL
  65. set(key: string, data: any, ttl?: number): void {
  66. this.cache.set(key, {
  67. data,
  68. timestamp: Date.now(),
  69. ttl: ttl || this.defaultTtl
  70. });
  71. }
  72. get(key: string): any | null {
  73. const cached = this.cache.get(key);
  74. if (!cached) return null;
  75. if (Date.now() - cached.timestamp > cached.ttl) {
  76. this.cache.delete(key);
  77. return null;
  78. }
  79. return cached.data;
  80. }
  81. clear(): void {
  82. this.cache.clear();
  83. }
  84. delete(key: string): void {
  85. this.cache.delete(key);
  86. }
  87. }
  88. const cache = new ApiCache();
  89. // Get configuration from server-injected config or fallback to environment/defaults
  90. // This function is called at runtime to ensure __SERVER_CONFIG__ is available
  91. function getApiConfig() {
  92. if (typeof window !== 'undefined' && window.__SERVER_CONFIG__) {
  93. return {
  94. apiUrl: window.__SERVER_CONFIG__.apiUrl,
  95. apiBase: window.__SERVER_CONFIG__.apiBasePath,
  96. };
  97. }
  98. // Fallback for development mode - use current window location
  99. if (typeof window !== 'undefined') {
  100. const protocol = window.location.protocol;
  101. const host = window.location.hostname;
  102. const port = window.location.port;
  103. return {
  104. apiUrl: `${protocol}//${host}:${port}`,
  105. apiBase: '/api',
  106. };
  107. }
  108. // Server-side fallback
  109. return {
  110. apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8081',
  111. apiBase: process.env.NEXT_PUBLIC_API_BASE_PATH || '/api',
  112. };
  113. }
  114. export interface GenerationRequest {
  115. model?: string;
  116. prompt: string;
  117. negative_prompt?: string;
  118. width?: number;
  119. height?: number;
  120. steps?: number;
  121. cfg_scale?: number;
  122. seed?: string;
  123. sampling_method?: string;
  124. scheduler?: string;
  125. batch_count?: number;
  126. clip_skip?: number;
  127. strength?: number;
  128. control_strength?: number;
  129. }
  130. export interface JobInfo {
  131. id?: string;
  132. request_id?: string;
  133. status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'queued';
  134. progress?: number;
  135. result?: {
  136. images: string[];
  137. };
  138. outputs?: Array<{
  139. filename: string;
  140. url: string;
  141. path: string;
  142. }>;
  143. error?: string;
  144. created_at?: string;
  145. updated_at?: string;
  146. message?: string;
  147. queue_position?: number;
  148. }
  149. export interface ModelInfo {
  150. id?: string;
  151. name: string;
  152. path?: string;
  153. type: string;
  154. size?: number;
  155. file_size?: number;
  156. file_size_mb?: number;
  157. sha256?: string | null;
  158. sha256_short?: string | null;
  159. loaded?: boolean;
  160. }
  161. export interface QueueStatus {
  162. active_generations: number;
  163. jobs: JobInfo[];
  164. running: boolean;
  165. size: number;
  166. }
  167. export interface HealthStatus {
  168. status: 'ok' | 'error' | 'degraded';
  169. message: string;
  170. timestamp: string;
  171. uptime?: number;
  172. version?: string;
  173. }
  174. class ApiClient {
  175. private baseUrl: string = '';
  176. private isInitialized: boolean = false;
  177. // Initialize base URL
  178. private initBaseUrl(): string {
  179. if (!this.isInitialized) {
  180. const config = getApiConfig();
  181. this.baseUrl = `${config.apiUrl}${config.apiBase}`;
  182. this.isInitialized = true;
  183. }
  184. return this.baseUrl;
  185. }
  186. // Get base URL dynamically at runtime to ensure server config is loaded
  187. private getBaseUrl(): string {
  188. return this.initBaseUrl();
  189. }
  190. private async request<T>(
  191. endpoint: string,
  192. options: RequestInit = {}
  193. ): Promise<T> {
  194. const url = `${this.getBaseUrl()}${endpoint}`;
  195. // Check request throttling for certain endpoints
  196. const needsThrottling = endpoint.includes('/queue/status') || endpoint.includes('/health');
  197. if (needsThrottling) {
  198. const waitTime = throttler.getWaitTime(endpoint);
  199. if (waitTime > 0) {
  200. // Wait before making the request
  201. await new Promise(resolve => setTimeout(resolve, waitTime));
  202. }
  203. if (!throttler.canMakeRequest(endpoint)) {
  204. throw new Error('Too many requests. Please wait before making another request.');
  205. }
  206. }
  207. // Get authentication method from server config
  208. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  209. ? window.__SERVER_CONFIG__.authMethod
  210. : 'jwt';
  211. // Add auth token or Unix user header based on auth method
  212. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  213. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  214. const headers: Record<string, string> = {
  215. 'Content-Type': 'application/json',
  216. ...options.headers as Record<string, string>,
  217. };
  218. if (authMethod === 'unix' && unixUser) {
  219. // For Unix auth, send the username in X-Unix-User header
  220. headers['X-Unix-User'] = unixUser;
  221. } else if (token) {
  222. // For JWT auth, send the token in Authorization header
  223. headers['Authorization'] = `Bearer ${token}`;
  224. }
  225. const response = await fetch(url, {
  226. ...options,
  227. headers,
  228. });
  229. if (!response.ok) {
  230. const errorData = await response.json().catch(() => ({
  231. error: { message: response.statusText },
  232. }));
  233. // Handle nested error structure: { error: { message: "..." } }
  234. const errorMessage =
  235. errorData.error?.message ||
  236. errorData.message ||
  237. errorData.error ||
  238. 'API request failed';
  239. throw new Error(errorMessage);
  240. }
  241. return response.json();
  242. }
  243. // Enhanced health check with caching and better error handling
  244. async checkHealth(): Promise<HealthStatus> {
  245. const cacheKey = 'health_check';
  246. const cachedResult = cache.get(cacheKey);
  247. if (cachedResult) {
  248. return cachedResult;
  249. }
  250. const endpoints = ['/queue/status', '/health', '/status', '/'];
  251. for (const endpoint of endpoints) {
  252. try {
  253. const response = await fetch(`${this.getBaseUrl()}${endpoint}`, {
  254. method: 'GET',
  255. headers: {
  256. 'Content-Type': 'application/json',
  257. },
  258. // Add timeout to prevent hanging requests
  259. signal: AbortSignal.timeout(3000), // Reduced timeout
  260. });
  261. if (response.ok) {
  262. const data = await response.json();
  263. // For queue status, consider it healthy if it returns valid structure
  264. if (endpoint === '/queue/status' && data.queue) {
  265. const result = {
  266. status: 'ok' as const,
  267. message: 'API is running and queue is accessible',
  268. timestamp: new Date().toISOString(),
  269. };
  270. cache.set(cacheKey, result, 10000); // Cache for 10 seconds
  271. return result;
  272. }
  273. // For other health endpoints
  274. const healthStatus: HealthStatus = {
  275. status: 'ok',
  276. message: 'API is running',
  277. timestamp: new Date().toISOString(),
  278. uptime: data.uptime,
  279. version: data.version || data.build || data.git_version,
  280. };
  281. // Handle different response formats
  282. if (data.status) {
  283. if (data.status === 'healthy' || data.status === 'running' || data.status === 'ok') {
  284. healthStatus.status = 'ok';
  285. healthStatus.message = data.message || 'API is running';
  286. } else if (data.status === 'degraded') {
  287. healthStatus.status = 'degraded';
  288. healthStatus.message = data.message || 'API is running in degraded mode';
  289. } else {
  290. healthStatus.status = 'error';
  291. healthStatus.message = data.message || 'API status is unknown';
  292. }
  293. }
  294. cache.set(cacheKey, healthStatus, 10000); // Cache for 10 seconds
  295. return healthStatus;
  296. }
  297. } catch (error) {
  298. // Continue to next endpoint if this one fails
  299. console.warn(`Health check failed for endpoint ${endpoint}:`, error);
  300. continue;
  301. }
  302. }
  303. // If all endpoints fail
  304. throw new Error('All health check endpoints are unavailable');
  305. }
  306. // Alternative simple connectivity check with caching
  307. async checkConnectivity(): Promise<boolean> {
  308. const cacheKey = 'connectivity_check';
  309. const cachedResult = cache.get(cacheKey);
  310. if (cachedResult !== null) {
  311. return cachedResult;
  312. }
  313. try {
  314. const response = await fetch(`${this.getBaseUrl()}`, {
  315. method: 'HEAD',
  316. signal: AbortSignal.timeout(2000), // Reduced timeout
  317. });
  318. const result = response.ok || response.status < 500;
  319. cache.set(cacheKey, result, 5000); // Cache for 5 seconds
  320. return result;
  321. } catch (error) {
  322. cache.set(cacheKey, false, 5000); // Cache failure for 5 seconds
  323. return false;
  324. }
  325. }
  326. // Generation endpoints
  327. async generateImage(params: GenerationRequest): Promise<JobInfo> {
  328. return this.request<JobInfo>('/generate/text2img', {
  329. method: 'POST',
  330. body: JSON.stringify(params),
  331. });
  332. }
  333. async text2img(params: GenerationRequest): Promise<JobInfo> {
  334. return this.request<JobInfo>('/generate/text2img', {
  335. method: 'POST',
  336. body: JSON.stringify(params),
  337. });
  338. }
  339. async img2img(params: GenerationRequest & { image: string }): Promise<JobInfo> {
  340. // Convert frontend field name to backend field name
  341. const backendParams = {
  342. ...params,
  343. init_image: params.image,
  344. image: undefined, // Remove the frontend field
  345. };
  346. return this.request<JobInfo>('/generate/img2img', {
  347. method: 'POST',
  348. body: JSON.stringify(backendParams),
  349. });
  350. }
  351. async inpainting(params: GenerationRequest & { source_image: string; mask_image: string }): Promise<JobInfo> {
  352. return this.request<JobInfo>('/generate/inpainting', {
  353. method: 'POST',
  354. body: JSON.stringify(params),
  355. });
  356. }
  357. // Job management with caching for status checks
  358. async getJobStatus(jobId: string): Promise<JobInfo> {
  359. const cacheKey = `job_status_${jobId}`;
  360. const cachedResult = cache.get(cacheKey);
  361. if (cachedResult) {
  362. return cachedResult;
  363. }
  364. const result = await this.request<JobInfo>(`/queue/job/${jobId}`);
  365. // Cache job status for a short time
  366. if (result.status === 'processing' || result.status === 'queued') {
  367. cache.set(cacheKey, result, 2000); // Cache for 2 seconds for active jobs
  368. } else {
  369. cache.set(cacheKey, result, 10000); // Cache for 10 seconds for completed jobs
  370. }
  371. return result;
  372. }
  373. // Get authenticated image URL with cache-busting
  374. getImageUrl(jobId: string, filename: string): string {
  375. const baseUrl = this.getBaseUrl();
  376. // Add cache-busting timestamp
  377. const timestamp = Date.now();
  378. const url = `${baseUrl}/queue/job/${jobId}/output/${filename}?t=${timestamp}`;
  379. return url;
  380. }
  381. // Download image with authentication
  382. async downloadImage(jobId: string, filename: string): Promise<Blob> {
  383. const url = this.getImageUrl(jobId, filename);
  384. // Get authentication method from server config
  385. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  386. ? window.__SERVER_CONFIG__.authMethod
  387. : 'jwt';
  388. // Add auth token or Unix user header based on auth method
  389. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  390. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  391. const headers: Record<string, string> = {};
  392. if (authMethod === 'unix' && unixUser) {
  393. // For Unix auth, send the username in X-Unix-User header
  394. headers['X-Unix-User'] = unixUser;
  395. } else if (token) {
  396. // For JWT auth, send the token in Authorization header
  397. headers['Authorization'] = `Bearer ${token}`;
  398. }
  399. const response = await fetch(url, {
  400. headers,
  401. });
  402. if (!response.ok) {
  403. const errorData = await response.json().catch(() => ({
  404. error: { message: response.statusText },
  405. }));
  406. // Handle nested error structure: { error: { message: "..." } }
  407. const errorMessage =
  408. errorData.error?.message ||
  409. errorData.message ||
  410. errorData.error ||
  411. 'Failed to download image';
  412. throw new Error(errorMessage);
  413. }
  414. return response.blob();
  415. }
  416. // Download image from URL with server-side proxy to avoid CORS issues
  417. async downloadImageFromUrl(url: string): Promise<{
  418. mimeType: string;
  419. filename: string;
  420. base64Data: string;
  421. }> {
  422. const apiUrl = `${this.getBaseUrl()}/image/download?url=${encodeURIComponent(url)}`;
  423. const response = await fetch(apiUrl);
  424. if (!response.ok) {
  425. const errorData = await response.json().catch(() => ({
  426. error: { message: response.statusText },
  427. }));
  428. // Handle nested error structure: { error: { message: "..." } }
  429. const errorMessage =
  430. errorData.error?.message ||
  431. errorData.message ||
  432. errorData.error ||
  433. 'Failed to download image from URL';
  434. throw new Error(errorMessage);
  435. }
  436. const result = await response.json();
  437. return {
  438. mimeType: result.mime_type,
  439. filename: result.filename,
  440. base64Data: result.base64_data
  441. };
  442. }
  443. async cancelJob(jobId: string): Promise<void> {
  444. // Clear job status cache when cancelling
  445. cache.delete(`job_status_${jobId}`);
  446. return this.request<void>('/queue/cancel', {
  447. method: 'POST',
  448. body: JSON.stringify({ job_id: jobId }),
  449. });
  450. }
  451. // Get queue status with caching and throttling
  452. async getQueueStatus(): Promise<QueueStatus> {
  453. const cacheKey = 'queue_status';
  454. const cachedResult = cache.get(cacheKey);
  455. if (cachedResult) {
  456. return cachedResult;
  457. }
  458. const response = await this.request<{ queue: QueueStatus }>('/queue/status');
  459. // Cache queue status based on current activity
  460. const hasActiveJobs = response.queue.jobs.some(job =>
  461. job.status === 'processing' || job.status === 'queued'
  462. );
  463. // Cache for shorter time if there are active jobs
  464. const cacheTime = hasActiveJobs ? 1000 : 5000; // 1 second for active, 5 seconds for idle
  465. cache.set(cacheKey, response.queue, cacheTime);
  466. return response.queue;
  467. }
  468. async clearQueue(): Promise<void> {
  469. // Clear all related caches
  470. cache.delete('queue_status');
  471. return this.request<void>('/queue/clear', {
  472. method: 'POST',
  473. });
  474. }
  475. // Model management
  476. async getModels(type?: string, loaded?: boolean, page: number = 1, limit: number = -1, search?: string): Promise<{ models: ModelInfo[]; pagination: any; statistics: any }> {
  477. const cacheKey = `models_${type || 'all'}_${loaded ? 'loaded' : 'all'}_${page}_${limit}_${search || 'all'}`;
  478. const cachedResult = cache.get(cacheKey);
  479. if (cachedResult) {
  480. return cachedResult;
  481. }
  482. let endpoint = '/models';
  483. const params = [];
  484. if (type && type !== 'loaded') params.push(`type=${type}`);
  485. if (type === 'loaded' || loaded) params.push('loaded=true');
  486. // Only add page parameter if we're using pagination (limit > 0)
  487. if (limit > 0) {
  488. params.push(`page=${page}`);
  489. params.push(`limit=${limit}`);
  490. } else {
  491. // When limit is 0 (default), we want all models, so add limit=0 to disable pagination
  492. params.push('limit=0');
  493. }
  494. if (search) params.push(`search=${encodeURIComponent(search)}`);
  495. // Add include_metadata for additional information
  496. params.push('include_metadata=true');
  497. if (params.length > 0) endpoint += '?' + params.join('&');
  498. const response = await this.request<{
  499. models: ModelInfo[];
  500. pagination: {
  501. page: number;
  502. limit: number;
  503. total_count: number;
  504. total_pages: number;
  505. has_next: boolean;
  506. has_prev: boolean
  507. };
  508. statistics: any;
  509. }>(endpoint);
  510. const models = response.models.map(model => ({
  511. ...model,
  512. id: model.sha256_short || model.name,
  513. size: model.file_size || model.size,
  514. path: model.path || model.name,
  515. }));
  516. const result = {
  517. models,
  518. pagination: response.pagination,
  519. statistics: response.statistics || {}
  520. };
  521. // Cache models for 30 seconds as they don't change frequently
  522. cache.set(cacheKey, result, 30000);
  523. return result;
  524. }
  525. // Get all models (for backward compatibility)
  526. async getAllModels(type?: string, loaded?: boolean): Promise<ModelInfo[]> {
  527. const allModels: ModelInfo[] = [];
  528. let page = 1;
  529. const limit = 100;
  530. while (true) {
  531. const response = await this.getModels(type, loaded, page, limit);
  532. allModels.push(...response.models);
  533. if (!response.pagination.has_next) {
  534. break;
  535. }
  536. page++;
  537. }
  538. return allModels;
  539. }
  540. async getModelInfo(modelId: string): Promise<ModelInfo> {
  541. const cacheKey = `model_info_${modelId}`;
  542. const cachedResult = cache.get(cacheKey);
  543. if (cachedResult) {
  544. return cachedResult;
  545. }
  546. const result = await this.request<ModelInfo>(`/models/${modelId}`);
  547. cache.set(cacheKey, result, 30000); // Cache for 30 seconds
  548. return result;
  549. }
  550. async loadModel(modelId: string): Promise<void> {
  551. // Clear model cache when loading
  552. cache.delete(`model_info_${modelId}`);
  553. return this.request<void>(`/models/${modelId}/load`, {
  554. method: 'POST',
  555. });
  556. }
  557. async unloadModel(modelId: string): Promise<void> {
  558. // Clear model cache when unloading
  559. cache.delete(`model_info_${modelId}`);
  560. return this.request<void>(`/models/${modelId}/unload`, {
  561. method: 'POST',
  562. });
  563. }
  564. async scanModels(): Promise<void> {
  565. // Clear all model caches when scanning
  566. cache.clear();
  567. return this.request<void>('/models/refresh', {
  568. method: 'POST',
  569. });
  570. }
  571. async getModelTypes(): Promise<Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string }>> {
  572. const cacheKey = 'model_types';
  573. const cachedResult = cache.get(cacheKey);
  574. if (cachedResult) {
  575. return cachedResult;
  576. }
  577. const response = await this.request<{ model_types: Array<{ type: string; description: string; extensions: string[]; capabilities: string[]; requires?: string[]; recommended_for: string }> }>('/models/types');
  578. cache.set(cacheKey, response.model_types, 60000); // Cache for 1 minute
  579. return response.model_types;
  580. }
  581. async convertModel(modelName: string, quantizationType: string, outputPath?: string): Promise<{ request_id: string; message: string }> {
  582. return this.request<{ request_id: string; message: string }>('/models/convert', {
  583. method: 'POST',
  584. body: JSON.stringify({
  585. model_name: modelName,
  586. quantization_type: quantizationType,
  587. output_path: outputPath,
  588. }),
  589. });
  590. }
  591. // System endpoints
  592. async getHealth(): Promise<{ status: string }> {
  593. return this.request<{ status: string }>('/health');
  594. }
  595. async getStatus(): Promise<any> {
  596. return this.request<any>('/status');
  597. }
  598. async getSystemInfo(): Promise<any> {
  599. return this.request<any>('/system');
  600. }
  601. async restartServer(): Promise<{ message: string }> {
  602. return this.request<{ message: string }>('/system/restart', {
  603. method: 'POST',
  604. body: JSON.stringify({}),
  605. });
  606. }
  607. // Image manipulation endpoints
  608. async resizeImage(image: string, width: number, height: number): Promise<{ image: string }> {
  609. return this.request<{ image: string }>('/image/resize', {
  610. method: 'POST',
  611. body: JSON.stringify({
  612. image,
  613. width,
  614. height,
  615. }),
  616. });
  617. }
  618. async cropImage(image: string, x: number, y: number, width: number, height: number): Promise<{ image: string }> {
  619. return this.request<{ image: string }>('/image/crop', {
  620. method: 'POST',
  621. body: JSON.stringify({
  622. image,
  623. x,
  624. y,
  625. width,
  626. height,
  627. }),
  628. });
  629. }
  630. // Configuration endpoints with caching
  631. async getSamplers(): Promise<Array<{ name: string; description: string; recommended_steps: number }>> {
  632. const cacheKey = 'samplers';
  633. const cachedResult = cache.get(cacheKey);
  634. if (cachedResult) {
  635. return cachedResult;
  636. }
  637. const response = await this.request<{ samplers: Array<{ name: string; description: string; recommended_steps: number }> }>('/samplers');
  638. cache.set(cacheKey, response.samplers, 60000); // Cache for 1 minute
  639. return response.samplers;
  640. }
  641. async getSchedulers(): Promise<Array<{ name: string; description: string }>> {
  642. const cacheKey = 'schedulers';
  643. const cachedResult = cache.get(cacheKey);
  644. if (cachedResult) {
  645. return cachedResult;
  646. }
  647. const response = await this.request<{ schedulers: Array<{ name: string; description: string }> }>('/schedulers');
  648. cache.set(cacheKey, response.schedulers, 60000); // Cache for 1 minute
  649. return response.schedulers;
  650. }
  651. // Cache management methods
  652. clearCache(): void {
  653. cache.clear();
  654. }
  655. clearCacheByPrefix(prefix: string): void {
  656. const keysToDelete: string[] = [];
  657. (cache as any).cache.forEach((_: any, key: string) => {
  658. if (key.startsWith(prefix)) {
  659. keysToDelete.push(key);
  660. }
  661. });
  662. keysToDelete.forEach(key => cache.delete(key));
  663. }
  664. }
  665. // Generic API request function for authentication
  666. export async function apiRequest(
  667. endpoint: string,
  668. options: RequestInit = {}
  669. ): Promise<Response> {
  670. const { apiUrl, apiBase } = getApiConfig();
  671. const url = `${apiUrl}${apiBase}${endpoint}`;
  672. // Get authentication method from server config
  673. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  674. ? window.__SERVER_CONFIG__.authMethod
  675. : 'jwt';
  676. // Add auth token or Unix user header based on auth method
  677. const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
  678. const unixUser = typeof window !== 'undefined' ? localStorage.getItem('unix_user') : null;
  679. const headers: Record<string, string> = {
  680. 'Content-Type': 'application/json',
  681. ...options.headers as Record<string, string>,
  682. };
  683. if (authMethod === 'unix' && unixUser) {
  684. // For Unix auth, send the username in X-Unix-User header
  685. headers['X-Unix-User'] = unixUser;
  686. } else if (token) {
  687. // For JWT auth, send the token in Authorization header
  688. headers['Authorization'] = `Bearer ${token}`;
  689. }
  690. return fetch(url, {
  691. ...options,
  692. headers,
  693. });
  694. }
  695. // Authentication API endpoints
  696. export const authApi = {
  697. async login(username: string, password?: string) {
  698. // Get authentication method from server config
  699. const authMethod = typeof window !== 'undefined' && window.__SERVER_CONFIG__
  700. ? window.__SERVER_CONFIG__.authMethod
  701. : 'jwt';
  702. // For both Unix and JWT auth, send username and password
  703. // The server will handle whether password is required based on PAM availability
  704. const response = await apiRequest('/auth/login', {
  705. method: 'POST',
  706. body: JSON.stringify({ username, password }),
  707. });
  708. if (!response.ok) {
  709. const error = await response.json().catch(() => ({ message: 'Login failed' }));
  710. throw new Error(error.message || 'Login failed');
  711. }
  712. return response.json();
  713. },
  714. async validateToken(token: string) {
  715. const response = await apiRequest('/auth/validate', {
  716. headers: { 'Authorization': `Bearer ${token}` },
  717. });
  718. if (!response.ok) {
  719. throw new Error('Token validation failed');
  720. }
  721. return response.json();
  722. },
  723. async refreshToken() {
  724. const response = await apiRequest('/auth/refresh', {
  725. method: 'POST',
  726. });
  727. if (!response.ok) {
  728. throw new Error('Token refresh failed');
  729. }
  730. return response.json();
  731. },
  732. async logout() {
  733. await apiRequest('/auth/logout', {
  734. method: 'POST',
  735. });
  736. },
  737. async getCurrentUser() {
  738. const response = await apiRequest('/auth/me');
  739. if (!response.ok) {
  740. throw new Error('Failed to get current user');
  741. }
  742. return response.json();
  743. },
  744. async changePassword(oldPassword: string, newPassword: string) {
  745. const response = await apiRequest('/auth/change-password', {
  746. method: 'POST',
  747. body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }),
  748. });
  749. if (!response.ok) {
  750. const error = await response.json().catch(() => ({ message: 'Password change failed' }));
  751. throw new Error(error.message || 'Password change failed');
  752. }
  753. return response.json();
  754. },
  755. // API Key management
  756. async getApiKeys() {
  757. const response = await apiRequest('/auth/api-keys');
  758. if (!response.ok) {
  759. throw new Error('Failed to get API keys');
  760. }
  761. return response.json();
  762. },
  763. async createApiKey(name: string, scopes?: string[]) {
  764. const response = await apiRequest('/auth/api-keys', {
  765. method: 'POST',
  766. body: JSON.stringify({ name, scopes }),
  767. });
  768. if (!response.ok) {
  769. const error = await response.json().catch(() => ({ message: 'Failed to create API key' }));
  770. throw new Error(error.message || 'Failed to create API key');
  771. }
  772. return response.json();
  773. },
  774. async deleteApiKey(keyId: string) {
  775. const response = await apiRequest(`/auth/api-keys/${keyId}`, {
  776. method: 'DELETE',
  777. });
  778. if (!response.ok) {
  779. throw new Error('Failed to delete API key');
  780. }
  781. return response.json();
  782. },
  783. // User management (admin only)
  784. async getUsers() {
  785. const response = await apiRequest('/auth/users');
  786. if (!response.ok) {
  787. throw new Error('Failed to get users');
  788. }
  789. return response.json();
  790. },
  791. async createUser(userData: { username: string; email?: string; password: string; role?: string }) {
  792. const response = await apiRequest('/auth/users', {
  793. method: 'POST',
  794. body: JSON.stringify(userData),
  795. });
  796. if (!response.ok) {
  797. const error = await response.json().catch(() => ({ message: 'Failed to create user' }));
  798. throw new Error(error.message || 'Failed to create user');
  799. }
  800. return response.json();
  801. },
  802. async updateUser(userId: string, userData: { email?: string; role?: string; active?: boolean }) {
  803. const response = await apiRequest(`/auth/users/${userId}`, {
  804. method: 'PUT',
  805. body: JSON.stringify(userData),
  806. });
  807. if (!response.ok) {
  808. const error = await response.json().catch(() => ({ message: 'Failed to update user' }));
  809. throw new Error(error.message || 'Failed to update user');
  810. }
  811. return response.json();
  812. },
  813. async deleteUser(userId: string) {
  814. const response = await apiRequest(`/auth/users/${userId}`, {
  815. method: 'DELETE',
  816. });
  817. if (!response.ok) {
  818. throw new Error('Failed to delete user');
  819. }
  820. return response.json();
  821. }
  822. };
  823. export const apiClient = new ApiClient();