Fix login loop on self-hosted desktop (#5022)

# Description of Changes
Fix #5017 

Changes the handling of jwt tokens to be stored in local storage as well
as OS keyring so the rest of the app knows that you're logged in.
This commit is contained in:
James Brunton
2025-11-26 13:51:12 +00:00
committed by GitHub
parent 9fdb5295cb
commit 7629d89356
5 changed files with 150 additions and 39 deletions
+87 -14
View File
@@ -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<void> {
// 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<string | null> {
// Try Tauri store first
console.log('[Desktop AuthService] Retrieving token from Tauri store...');
const token = await invoke<string | null>('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<void> {
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<string | null> {
try {
const token = await invoke<string | null>('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<UserInfo | null> {
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<UserInfo | null>('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<void> {
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);