diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 568ca1829..54b24b244 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -18,7 +18,7 @@ */ import { useEffect, useState, useRef } from 'react'; -import { getHttpApiClient } from '@/lib/http-api-client'; +import { getHttpApiClient, waitForApiKeyInit } from '@/lib/http-api-client'; import { isElectron } from '@/lib/electron'; import { getItem, removeItem } from '@/lib/storage'; import { useAppStore } from '@/store/app-store'; @@ -99,6 +99,10 @@ export function useSettingsMigration(): MigrationState { } try { + // Wait for API key to be initialized before making any API calls + // This prevents 401 errors on startup in Electron mode + await waitForApiKeyInit(); + const api = getHttpApiClient(); // Check if server has settings files diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index b856bd513..296fcfb16 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -44,6 +44,7 @@ const getServerUrl = (): string => { // Cached API key for authentication (Electron mode only) let cachedApiKey: string | null = null; let apiKeyInitialized = false; +let apiKeyInitPromise: Promise | null = null; // Cached session token for authentication (Web mode - explicit header auth) let cachedSessionToken: string | null = null; @@ -52,6 +53,17 @@ let cachedSessionToken: string | null = null; // Exported for use in WebSocket connections that need auth export const getApiKey = (): string | null => cachedApiKey; +/** + * Wait for API key initialization to complete. + * Returns immediately if already initialized. + */ +export const waitForApiKeyInit = (): Promise => { + if (apiKeyInitialized) return Promise.resolve(); + if (apiKeyInitPromise) return apiKeyInitPromise; + // If not started yet, start it now + return initApiKey(); +}; + // Get session token for Web mode (returns cached value after login or token fetch) export const getSessionToken = (): string | null => cachedSessionToken; @@ -79,24 +91,37 @@ export const isElectronMode = (): boolean => { * This should be called early in app initialization. */ export const initApiKey = async (): Promise => { + // Return existing promise if already in progress + if (apiKeyInitPromise) return apiKeyInitPromise; + + // Return immediately if already initialized if (apiKeyInitialized) return; - apiKeyInitialized = true; - // Only Electron mode uses API key header auth - if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) { + // Create and store the promise so concurrent calls wait for the same initialization + apiKeyInitPromise = (async () => { try { - cachedApiKey = await window.electronAPI.getApiKey(); - if (cachedApiKey) { - console.log('[HTTP Client] Using API key from Electron'); - return; + // Only Electron mode uses API key header auth + if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) { + try { + cachedApiKey = await window.electronAPI.getApiKey(); + if (cachedApiKey) { + console.log('[HTTP Client] Using API key from Electron'); + return; + } + } catch (error) { + console.warn('[HTTP Client] Failed to get API key from Electron:', error); + } } - } catch (error) { - console.warn('[HTTP Client] Failed to get API key from Electron:', error); + + // In web mode, authentication is handled via HTTP-only cookies + console.log('[HTTP Client] Web mode - using cookie-based authentication'); + } finally { + // Mark as initialized after completion, regardless of success or failure + apiKeyInitialized = true; } - } + })(); - // In web mode, authentication is handled via HTTP-only cookies - console.log('[HTTP Client] Web mode - using cookie-based authentication'); + return apiKeyInitPromise; }; /** @@ -296,7 +321,17 @@ export class HttpApiClient implements ElectronAPI { constructor() { this.serverUrl = getServerUrl(); - this.connectWebSocket(); + // Wait for API key initialization before connecting WebSocket + // This prevents 401 errors on startup in Electron mode + waitForApiKeyInit() + .then(() => { + this.connectWebSocket(); + }) + .catch((error) => { + console.error('[HttpApiClient] API key initialization failed:', error); + // Still attempt WebSocket connection - it may work with cookie auth + this.connectWebSocket(); + }); } /** @@ -460,6 +495,8 @@ export class HttpApiClient implements ElectronAPI { } private async post(endpoint: string, body?: unknown): Promise { + // Ensure API key is initialized before making request + await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { method: 'POST', headers: this.getHeaders(), @@ -470,6 +507,8 @@ export class HttpApiClient implements ElectronAPI { } private async get(endpoint: string): Promise { + // Ensure API key is initialized before making request + await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { headers: this.getHeaders(), credentials: 'include', // Include cookies for session auth @@ -478,6 +517,8 @@ export class HttpApiClient implements ElectronAPI { } private async put(endpoint: string, body?: unknown): Promise { + // Ensure API key is initialized before making request + await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { method: 'PUT', headers: this.getHeaders(), @@ -488,6 +529,8 @@ export class HttpApiClient implements ElectronAPI { } private async httpDelete(endpoint: string): Promise { + // Ensure API key is initialized before making request + await waitForApiKeyInit(); const response = await fetch(`${this.serverUrl}${endpoint}`, { method: 'DELETE', headers: this.getHeaders(),