mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
fix: resolve auth race condition causing 401 errors on Electron startup
API requests were being made before initApiKey() completed, causing 401 Unauthorized errors on app startup in Electron mode. Changes: - Add waitForApiKeyInit() to track and await API key initialization - Make HTTP methods (get/post/put/delete) wait for auth before requests - Defer WebSocket connection until API key is ready - Add explicit auth wait in useSettingsMigration hook Fixes race condition introduced in PR #321
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from 'react';
|
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 { isElectron } from '@/lib/electron';
|
||||||
import { getItem, removeItem } from '@/lib/storage';
|
import { getItem, removeItem } from '@/lib/storage';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
@@ -99,6 +99,10 @@ export function useSettingsMigration(): MigrationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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();
|
const api = getHttpApiClient();
|
||||||
|
|
||||||
// Check if server has settings files
|
// Check if server has settings files
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const getServerUrl = (): string => {
|
|||||||
// Cached API key for authentication (Electron mode only)
|
// Cached API key for authentication (Electron mode only)
|
||||||
let cachedApiKey: string | null = null;
|
let cachedApiKey: string | null = null;
|
||||||
let apiKeyInitialized = false;
|
let apiKeyInitialized = false;
|
||||||
|
let apiKeyInitPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
// Cached session token for authentication (Web mode - explicit header auth)
|
// Cached session token for authentication (Web mode - explicit header auth)
|
||||||
let cachedSessionToken: string | null = null;
|
let cachedSessionToken: string | null = null;
|
||||||
@@ -52,6 +53,17 @@ let cachedSessionToken: string | null = null;
|
|||||||
// Exported for use in WebSocket connections that need auth
|
// Exported for use in WebSocket connections that need auth
|
||||||
export const getApiKey = (): string | null => cachedApiKey;
|
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)
|
// Get session token for Web mode (returns cached value after login or token fetch)
|
||||||
export const getSessionToken = (): string | null => cachedSessionToken;
|
export const getSessionToken = (): string | null => cachedSessionToken;
|
||||||
|
|
||||||
@@ -79,24 +91,37 @@ export const isElectronMode = (): boolean => {
|
|||||||
* This should be called early in app initialization.
|
* This should be called early in app initialization.
|
||||||
*/
|
*/
|
||||||
export const initApiKey = async (): Promise<void> => {
|
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;
|
if (apiKeyInitialized) return;
|
||||||
apiKeyInitialized = true;
|
|
||||||
|
|
||||||
// Only Electron mode uses API key header auth
|
// Create and store the promise so concurrent calls wait for the same initialization
|
||||||
if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) {
|
apiKeyInitPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
cachedApiKey = await window.electronAPI.getApiKey();
|
// Only Electron mode uses API key header auth
|
||||||
if (cachedApiKey) {
|
if (typeof window !== 'undefined' && window.electronAPI?.getApiKey) {
|
||||||
console.log('[HTTP Client] Using API key from Electron');
|
try {
|
||||||
return;
|
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
|
// In web mode, authentication is handled via HTTP-only cookies
|
||||||
console.log('[HTTP Client] Web mode - using cookie-based authentication');
|
console.log('[HTTP Client] Web mode - using cookie-based authentication');
|
||||||
|
} finally {
|
||||||
|
// Mark as initialized after completion, regardless of success or failure
|
||||||
|
apiKeyInitialized = true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return apiKeyInitPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,7 +321,17 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.serverUrl = getServerUrl();
|
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<T>(endpoint: string, body?: unknown): Promise<T> {
|
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}`, {
|
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
@@ -470,6 +507,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async get<T>(endpoint: string): Promise<T> {
|
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}`, {
|
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
credentials: 'include', // Include cookies for session auth
|
credentials: 'include', // Include cookies for session auth
|
||||||
@@ -478,6 +517,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async put<T>(endpoint: string, body?: unknown): Promise<T> {
|
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}`, {
|
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
@@ -488,6 +529,8 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async httpDelete<T>(endpoint: string): Promise<T> {
|
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}`, {
|
const response = await fetch(`${this.serverUrl}${endpoint}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
|
|||||||
Reference in New Issue
Block a user