import { invoke } from '@tauri-apps/api/core'; import axios from 'axios'; export interface UserInfo { username: string; email?: string; } interface LoginResponse { token: string; username: string; email: string | null; } export type AuthStatus = 'authenticated' | 'unauthenticated' | 'refreshing'; export class AuthService { private static instance: AuthService; private authStatus: AuthStatus = 'unauthenticated'; private userInfo: UserInfo | null = null; private authListeners = new Set<(status: AuthStatus, userInfo: UserInfo | null) => void>(); static getInstance(): AuthService { if (!AuthService.instance) { AuthService.instance = new AuthService(); } return AuthService.instance; } subscribeToAuth(listener: (status: AuthStatus, userInfo: UserInfo | null) => void): () => void { this.authListeners.add(listener); // Immediately notify new listener of current state listener(this.authStatus, this.userInfo); return () => { this.authListeners.delete(listener); }; } private notifyListeners() { this.authListeners.forEach(listener => listener(this.authStatus, this.userInfo)); } private setAuthStatus(status: AuthStatus, userInfo: UserInfo | null = null) { this.authStatus = status; this.userInfo = userInfo; this.notifyListeners(); } async login(serverUrl: string, username: string, password: string): Promise { try { console.log('Logging in to:', serverUrl); // Call Rust login command (bypasses CORS) const response = await invoke('login', { serverUrl, username, password, }); const { token, username: returnedUsername, email } = response; // Save the token to keyring await invoke('save_auth_token', { token }); // Save user info to store await invoke('save_user_info', { username: returnedUsername || username, email, }); const userInfo: UserInfo = { username: returnedUsername || username, email: email || undefined, }; this.setAuthStatus('authenticated', userInfo); console.log('Login successful'); return userInfo; } catch (error) { console.error('Login failed:', error); this.setAuthStatus('unauthenticated', null); // Rust commands return string errors if (typeof error === 'string') { throw new Error(error); } throw new Error('Login failed. Please try again.'); } } async logout(): Promise { try { console.log('Logging out'); // Clear token from keyring await invoke('clear_auth_token'); // Clear user info from store await invoke('clear_user_info'); this.setAuthStatus('unauthenticated', null); console.log('Logged out successfully'); } catch (error) { console.error('Error during logout:', error); // Still set status to unauthenticated even if clear fails this.setAuthStatus('unauthenticated', null); } } async getAuthToken(): Promise { try { const token = await invoke('get_auth_token'); return token || null; } catch (error) { console.error('Failed to get auth token:', error); return null; } } async isAuthenticated(): Promise { const token = await this.getAuthToken(); return token !== null; } async getUserInfo(): Promise { if (this.userInfo) { return this.userInfo; } try { const userInfo = await invoke('get_user_info'); this.userInfo = userInfo; return userInfo; } catch (error) { console.error('Failed to get user info:', error); return null; } } async refreshToken(serverUrl: string): Promise { try { console.log('Refreshing auth token'); this.setAuthStatus('refreshing', this.userInfo); const currentToken = await this.getAuthToken(); if (!currentToken) { this.setAuthStatus('unauthenticated', null); return false; } // Call the server's refresh endpoint const response = await axios.post( `${serverUrl}/api/v1/auth/refresh`, {}, { headers: { Authorization: `Bearer ${currentToken}`, }, } ); const { token } = response.data; // Save the new token await invoke('save_auth_token', { token }); const userInfo = await this.getUserInfo(); this.setAuthStatus('authenticated', userInfo); console.log('Token refreshed successfully'); return true; } catch (error) { console.error('Token refresh failed:', error); this.setAuthStatus('unauthenticated', null); // Clear stored credentials on refresh failure await this.logout(); return false; } } async initializeAuthState(): Promise { const token = await this.getAuthToken(); const userInfo = await this.getUserInfo(); if (token && userInfo) { this.setAuthStatus('authenticated', userInfo); } else { this.setAuthStatus('unauthenticated', null); } } } export const authService = AuthService.getInstance();