mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Allow desktop app to connect to selfhosted servers (#4902)
# 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 🙁
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user