diff --git a/frontend/src/desktop/services/authService.ts b/frontend/src/desktop/services/authService.ts index 56bb7c085..76f8aa157 100644 --- a/frontend/src/desktop/services/authService.ts +++ b/frontend/src/desktop/services/authService.ts @@ -34,6 +34,55 @@ export class AuthService { return AuthService.instance; } + /** + * Save token to all storage locations and notify listeners + */ + private async saveTokenEverywhere(token: string): Promise { + // Save to Tauri store + await invoke('save_auth_token', { token }); + console.log('[Desktop AuthService] Token saved to Tauri store'); + + // Sync to localStorage for web layer + localStorage.setItem('stirling_jwt', token); + console.log('[Desktop AuthService] Token saved to localStorage'); + + // Notify other parts of the system + window.dispatchEvent(new CustomEvent('jwt-available')); + console.log('[Desktop AuthService] Dispatched jwt-available event'); + } + + /** + * Get token from any available source (Tauri store or localStorage) + */ + private async getTokenFromAnySource(): Promise { + // Try Tauri store first + console.log('[Desktop AuthService] Retrieving token from Tauri store...'); + const token = await invoke('get_auth_token'); + + if (token) { + console.log(`[Desktop AuthService] Token found in Tauri store (length: ${token.length})`); + return token; + } + + console.log('[Desktop AuthService] No token in Tauri store'); + + // Fallback to localStorage + const localStorageToken = localStorage.getItem('stirling_jwt'); + if (localStorageToken) { + console.log('[Desktop AuthService] Token found in localStorage (length:', localStorageToken.length, ')'); + } + + return localStorageToken; + } + + /** + * Clear token from all storage locations + */ + private async clearTokenEverywhere(): Promise { + await invoke('clear_auth_token'); + localStorage.removeItem('stirling_jwt'); + } + subscribeToAuth(listener: (status: AuthStatus, userInfo: UserInfo | null) => void): () => void { this.authListeners.add(listener); // Immediately notify new listener of current state @@ -78,8 +127,15 @@ export class AuthService { const { token, username: returnedUsername, email } = response; - // Save the token to keyring - await invoke('save_auth_token', { token }); + console.log('[Desktop AuthService] Login successful, saving token...'); + + // Save token to all storage locations + try { + await this.saveTokenEverywhere(token); + } catch (error) { + console.error('[Desktop AuthService] Failed to save token:', error); + throw new Error('Failed to save authentication token'); + } // Save user info to store await invoke('save_user_info', { @@ -107,10 +163,10 @@ export class AuthService { try { console.log('Logging out'); - // Clear token from keyring - await invoke('clear_auth_token'); + // Clear token from all storage locations + await this.clearTokenEverywhere(); - // Clear user info from store + // Clear user info from Tauri store await invoke('clear_user_info'); this.setAuthStatus('unauthenticated', null); @@ -120,15 +176,16 @@ export class AuthService { console.error('Error during logout:', error); // Still set status to unauthenticated even if clear fails this.setAuthStatus('unauthenticated', null); + // Still try to clear token + await this.clearTokenEverywhere().catch(() => {}); } } async getAuthToken(): Promise { try { - const token = await invoke('get_auth_token'); - return token || null; + return await this.getTokenFromAnySource(); } catch (error) { - console.error('Failed to get auth token:', error); + console.error('[Desktop AuthService] Failed to get auth token:', error); return null; } } @@ -140,15 +197,22 @@ export class AuthService { async getUserInfo(): Promise { if (this.userInfo) { + console.log('[Desktop AuthService] Using cached user info:', this.userInfo.username); return this.userInfo; } try { + console.log('[Desktop AuthService] Retrieving user info from store...'); const userInfo = await invoke('get_user_info'); - this.userInfo = userInfo; + if (userInfo) { + console.log('[Desktop AuthService] User info found:', userInfo.username); + this.userInfo = userInfo; + } else { + console.log('[Desktop AuthService] No user info in store'); + } return userInfo; } catch (error) { - console.error('Failed to get user info:', error); + console.error('[Desktop AuthService] Failed to get user info from store:', error); return null; } } @@ -177,8 +241,8 @@ export class AuthService { const { token } = response.data; - // Save the new token - await invoke('save_auth_token', { token }); + // Save token to all storage locations + await this.saveTokenEverywhere(token); const userInfo = await this.getUserInfo(); this.setAuthStatus('authenticated', userInfo); @@ -197,13 +261,22 @@ export class AuthService { } async initializeAuthState(): Promise { + console.log('[Desktop AuthService] Initializing auth state...'); const token = await this.getAuthToken(); const userInfo = await this.getUserInfo(); if (token && userInfo) { + console.log('[Desktop AuthService] Found token, syncing to all storage locations'); + + // Ensure token is in both Tauri store and localStorage + await this.saveTokenEverywhere(token); + this.setAuthStatus('authenticated', userInfo); + console.log('[Desktop AuthService] Auth state initialized as authenticated'); } else { + console.log('[Desktop AuthService] No token or user info found'); this.setAuthStatus('unauthenticated', null); + console.log('[Desktop AuthService] Auth state initialized as unauthenticated'); } } @@ -235,8 +308,8 @@ export class AuthService { console.log('OAuth authentication successful, storing tokens'); - // Save the access token to keyring - await invoke('save_auth_token', { token: result.access_token }); + // Save token to all storage locations + await this.saveTokenEverywhere(result.access_token); // Fetch user info from Supabase using the access token const userInfo = await this.fetchSupabaseUserInfo(authServerUrl, result.access_token); diff --git a/frontend/src/desktop/services/tauriBackendService.ts b/frontend/src/desktop/services/tauriBackendService.ts index f78c72b8f..abd5dd236 100644 --- a/frontend/src/desktop/services/tauriBackendService.ts +++ b/frontend/src/desktop/services/tauriBackendService.ts @@ -117,6 +117,25 @@ export class TauriBackendService { throw new Error('Failed to detect backend port after 15 seconds'); } + /** + * Get auth token from any available source (localStorage or Tauri store) + */ + private async getAuthToken(): Promise { + // Check localStorage first (web layer token) + const localStorageToken = localStorage.getItem('stirling_jwt'); + if (localStorageToken) { + return localStorageToken; + } + + // Fallback to Tauri store + try { + return await invoke('get_auth_token'); + } catch { + console.debug('[TauriBackendService] No auth token available'); + return null; + } + } + private beginHealthMonitoring() { if (this.healthMonitor) { return; @@ -158,9 +177,20 @@ export class TauriBackendService { // Check if backend is ready (dependencies checked) try { const configUrl = `${baseUrl}/api/v1/config/app-config`; + + // For self-hosted mode, include auth token if available + const headers: Record = {}; + if (mode === 'selfhosted') { + const token = await this.getAuthToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + } + const response = await fetch(configUrl, { method: 'GET', connectTimeout: 5000, + headers, }); if (!response.ok) { diff --git a/frontend/src/proprietary/auth/UseSession.tsx b/frontend/src/proprietary/auth/UseSession.tsx index 748910e33..64e5baef0 100644 --- a/frontend/src/proprietary/auth/UseSession.tsx +++ b/frontend/src/proprietary/auth/UseSession.tsx @@ -126,6 +126,14 @@ export function AuthProvider({ children }: { children: ReactNode }) { initializeAuth(); + // Listen for jwt-available event (triggered by desktop auth or other sources) + const handleJwtAvailable = () => { + console.debug('[Auth] JWT available event received, refreshing session'); + void initializeAuth(); + }; + + window.addEventListener('jwt-available', handleJwtAvailable); + // Subscribe to auth state changes const { data: { subscription } } = springAuth.onAuthStateChange( async (event: AuthChangeEvent, newSession: Session | null) => { @@ -162,6 +170,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { return () => { mounted = false; + window.removeEventListener('jwt-available', handleJwtAvailable); subscription.unsubscribe(); }; }, []); diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index 51ba51df4..2f1aa36cb 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -161,8 +161,8 @@ class SpringAuthClient { return { data: { session: null }, error: null }; } - // Clear potentially invalid token on other errors too - localStorage.removeItem('stirling_jwt'); + // Don't clear token for other errors (e.g., backend not ready, network issues) + // The token is still valid, just can't verify it right now return { data: { session: null }, error: { message: getErrorMessage(error, 'Unknown error') }, diff --git a/frontend/src/proprietary/routes/Login.tsx b/frontend/src/proprietary/routes/Login.tsx index 4141a9933..4390131ca 100644 --- a/frontend/src/proprietary/routes/Login.tsx +++ b/frontend/src/proprietary/routes/Login.tsx @@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next'; import { useDocumentMeta } from '@app/hooks/useDocumentMeta'; import AuthLayout from '@app/routes/authShared/AuthLayout'; import { useBackendProbe } from '@app/hooks/useBackendProbe'; +import apiClient from '@app/services/apiClient'; +import { BASE_PATH } from '@app/constants/app'; // Import login components import LoginHeader from '@app/routes/login/LoginHeader'; @@ -16,7 +18,6 @@ import EmailPasswordForm from '@app/routes/login/EmailPasswordForm'; import OAuthButtons, { DEBUG_SHOW_ALL_PROVIDERS, oauthProviderConfig } from '@app/routes/login/OAuthButtons'; import DividerWithText from '@app/components/shared/DividerWithText'; import LoggedInState from '@app/routes/login/LoggedInState'; -import { BASE_PATH } from '@app/constants/app'; export default function Login() { const navigate = useNavigate(); @@ -85,30 +86,28 @@ export default function Login() { useEffect(() => { const fetchProviders = async () => { try { - const response = await fetch(`${BASE_PATH}/api/v1/proprietary/ui-data/login`); - if (response.ok) { - const data = await response.json(); + const response = await apiClient.get('/api/v1/proprietary/ui-data/login'); + const data = response.data; - // Check if login is disabled - if so, redirect to home - if (data.enableLogin === false) { - console.debug('[Login] Login disabled, redirecting to home'); - navigate('/'); - return; - } - - setEnableLogin(data.enableLogin ?? true); - - // Set first-time setup flags - setIsFirstTimeSetup(data.firstTimeSetup ?? false); - setShowDefaultCredentials(data.showDefaultCredentials ?? false); - - // Extract provider IDs from the providerList map - // The keys are like "/oauth2/authorization/google" - extract the last part - const providerIds = Object.keys(data.providerList || {}) - .map(key => key.split('/').pop()) - .filter((id): id is string => id !== undefined); - setEnabledProviders(providerIds); + // Check if login is disabled - if so, redirect to home + if (data.enableLogin === false) { + console.debug('[Login] Login disabled, redirecting to home'); + navigate('/'); + return; } + + setEnableLogin(data.enableLogin ?? true); + + // Set first-time setup flags + setIsFirstTimeSetup(data.firstTimeSetup ?? false); + setShowDefaultCredentials(data.showDefaultCredentials ?? false); + + // Extract provider IDs from the providerList map + // The keys are like "/oauth2/authorization/google" - extract the last part + const providerIds = Object.keys(data.providerList || {}) + .map(key => key.split('/').pop()) + .filter((id): id is string => id !== undefined); + setEnabledProviders(providerIds); } catch (err) { console.error('[Login] Failed to fetch enabled providers:', err); }