Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/ui/src/hooks/use-settings-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
69 changes: 56 additions & 13 deletions apps/ui/src/lib/http-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> | null = null;

// Cached session token for authentication (Web mode - explicit header auth)
let cachedSessionToken: string | null = null;
Expand All @@ -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<void> => {
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;

Expand Down Expand Up @@ -79,24 +91,37 @@ export const isElectronMode = (): boolean => {
* This should be called early in app initialization.
*/
export const initApiKey = async (): Promise<void> => {
// 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;
};

/**
Expand Down Expand Up @@ -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();
});
}

/**
Expand Down Expand Up @@ -460,6 +495,8 @@ export class HttpApiClient implements ElectronAPI {
}

private async post<T>(endpoint: string, body?: unknown): Promise<T> {
// Ensure API key is initialized before making request
await waitForApiKeyInit();
const response = await fetch(`${this.serverUrl}${endpoint}`, {
method: 'POST',
headers: this.getHeaders(),
Expand All @@ -470,6 +507,8 @@ export class HttpApiClient implements ElectronAPI {
}

private async get<T>(endpoint: string): Promise<T> {
// 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
Expand All @@ -478,6 +517,8 @@ export class HttpApiClient implements ElectronAPI {
}

private async put<T>(endpoint: string, body?: unknown): Promise<T> {
// Ensure API key is initialized before making request
await waitForApiKeyInit();
const response = await fetch(`${this.serverUrl}${endpoint}`, {
method: 'PUT',
headers: this.getHeaders(),
Expand All @@ -488,6 +529,8 @@ export class HttpApiClient implements ElectronAPI {
}

private async httpDelete<T>(endpoint: string): Promise<T> {
// Ensure API key is initialized before making request
await waitForApiKeyInit();
const response = await fetch(`${this.serverUrl}${endpoint}`, {
method: 'DELETE',
headers: this.getHeaders(),
Expand Down
Loading