Desktop to match normal login screens (#5122)1

Also fixed issue with csrf
Also fixed issue with rust keychain

---------

Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
ConnorYoh
2025-12-04 17:48:19 +00:00
committed by GitHub
co-authored by James Brunton
parent 7459463a3c
commit c6b4a2b141
28 changed files with 779 additions and 607 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ import { getApiBaseUrl } from '@app/services/apiClientConfig';
const apiClient = create({
baseURL: getApiBaseUrl(),
responseType: 'json',
withCredentials: true,
withCredentials: false, // Desktop doesn't need credentials
});
// Setup interceptors (desktop-specific auth and backend ready checks)
@@ -48,13 +48,21 @@ export function setupApiInterceptors(client: AxiosInstance): void {
// Debug logging
console.debug(`[apiClientSetup] Request to: ${extendedConfig.url}`);
// Add auth token for remote requests
// Add auth token for remote requests and enable credentials
const isRemote = await operationRouter.isSelfHostedMode();
if (isRemote) {
// Self-hosted mode: enable credentials for session management
extendedConfig.withCredentials = true;
const token = await authService.getAuthToken();
if (token) {
extendedConfig.headers.Authorization = `Bearer ${token}`;
} else {
console.warn('[apiClientSetup] Self-hosted mode but no auth token available');
}
} else {
// SaaS mode: disable credentials (security disabled on local backend)
extendedConfig.withCredentials = false;
}
// Backend readiness check (for local backend)
@@ -85,7 +93,9 @@ export function setupApiInterceptors(client: AxiosInstance): void {
// Response interceptor: Handle auth errors
client.interceptors.response.use(
(response) => response,
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config as ExtendedRequestConfig;
+59 -15
View File
@@ -25,6 +25,7 @@ export class AuthService {
private static instance: AuthService;
private authStatus: AuthStatus = 'unauthenticated';
private userInfo: UserInfo | null = null;
private cachedToken: string | null = null;
private authListeners = new Set<(status: AuthStatus, userInfo: UserInfo | null) => void>();
static getInstance(): AuthService {
@@ -38,13 +39,32 @@ export class AuthService {
* 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');
// Validate token before caching
if (!token || token.trim().length === 0) {
console.warn('[Desktop AuthService] Attempted to save invalid/empty token');
throw new Error('Invalid token');
}
// Sync to localStorage for web layer
localStorage.setItem('stirling_jwt', token);
console.log('[Desktop AuthService] Token saved to localStorage');
try {
// Save to Tauri store
await invoke('save_auth_token', { token });
console.log('[Desktop AuthService] ✅ Token saved to Tauri store');
} catch (error) {
console.error('[Desktop AuthService] ❌ Failed to save token to Tauri store:', error);
// Don't throw - we can still use localStorage
}
try {
// Sync to localStorage for web layer
localStorage.setItem('stirling_jwt', token);
console.log('[Desktop AuthService] ✅ Token saved to localStorage');
} catch (error) {
console.error('[Desktop AuthService] ❌ Failed to save token to localStorage:', error);
}
// Cache the valid token in memory
this.cachedToken = token;
console.log('[Desktop AuthService] ✅ Token cached in memory');
// Notify other parts of the system
window.dispatchEvent(new CustomEvent('jwt-available'));
@@ -56,20 +76,25 @@ export class AuthService {
*/
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');
try {
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;
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, checking localStorage...');
} catch (error) {
console.error('[Desktop AuthService] ❌ Failed to read from Tauri store:', error);
}
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, ')');
console.log(`[Desktop AuthService] Token found in localStorage (length: ${localStorageToken.length})`);
} else {
console.log('[Desktop AuthService] ❌ No token found in any storage');
}
return localStorageToken;
@@ -79,6 +104,10 @@ export class AuthService {
* Clear token from all storage locations
*/
private async clearTokenEverywhere(): Promise<void> {
// Invalidate cache
this.cachedToken = null;
console.log('[Desktop AuthService] Cache invalidated');
await invoke('clear_auth_token');
localStorage.removeItem('stirling_jwt');
}
@@ -183,7 +212,22 @@ export class AuthService {
async getAuthToken(): Promise<string | null> {
try {
return await this.getTokenFromAnySource();
// Return cached token if available
if (this.cachedToken) {
console.debug('[Desktop AuthService] ✅ Returning cached token');
return this.cachedToken;
}
console.debug('[Desktop AuthService] Cache miss, fetching from storage...');
const token = await this.getTokenFromAnySource();
// Cache the token if valid
if (token && token.trim().length > 0) {
this.cachedToken = token;
console.log('[Desktop AuthService] ✅ Token cached in memory after retrieval');
}
return token;
} catch (error) {
console.error('[Desktop AuthService] Failed to get auth token:', error);
return null;
@@ -5,6 +5,7 @@ export type ConnectionMode = 'saas' | 'selfhosted';
export interface ServerConfig {
url: string;
enabledOAuthProviders?: string[];
}
export interface ConnectionConfig {
@@ -61,7 +61,7 @@ class TauriHttpClient {
headers: {},
timeout: 120000,
responseType: 'json',
withCredentials: true,
withCredentials: false, // Desktop doesn't need credentials (backend has allowCredentials=false)
};
public interceptors: Interceptors = {
@@ -173,14 +173,15 @@ class TauriHttpClient {
}
try {
// Debug logging
console.debug(`[tauriHttpClient] Fetch request:`, { url, method });
// Convert withCredentials to fetch API's credentials option
const credentials: RequestCredentials = finalConfig.withCredentials ? 'include' : 'omit';
// Make the request using Tauri's native HTTP client (standard Fetch API)
const response = await fetch(url, {
method,
headers,
body,
credentials,
});
// Parse response based on responseType