mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-04 02:20:19 +01:00
# Description of Changes Changes the desktop app to allow connections to self-hosted servers on first startup. This was quite involved and hit loads of CORS issues all through the stack, but I think it's working now. This also changes the bundled backend to spawn on an OS-decided port rather than always spawning on `8080`, which means that the user can have other things running on port `8080` now and the app will still work fine. There were quite a few places that needed to be updated to decouple the app from explicitly using `8080` and I was originally going to split those changes out into another PR (#4939), but I couldn't get it working independently in the time I had, so the diff here is just going to be complex and contian two distinct changes - sorry 🙁
199 lines
5.1 KiB
TypeScript
199 lines
5.1 KiB
TypeScript
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<UserInfo> {
|
|
try {
|
|
console.log('Logging in to:', serverUrl);
|
|
|
|
// Call Rust login command (bypasses CORS)
|
|
const response = await invoke<LoginResponse>('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<void> {
|
|
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<string | null> {
|
|
try {
|
|
const token = await invoke<string | null>('get_auth_token');
|
|
return token || null;
|
|
} catch (error) {
|
|
console.error('Failed to get auth token:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async isAuthenticated(): Promise<boolean> {
|
|
const token = await this.getAuthToken();
|
|
return token !== null;
|
|
}
|
|
|
|
async getUserInfo(): Promise<UserInfo | null> {
|
|
if (this.userInfo) {
|
|
return this.userInfo;
|
|
}
|
|
|
|
try {
|
|
const userInfo = await invoke<UserInfo | null>('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<boolean> {
|
|
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<void> {
|
|
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();
|