ieeeucsd-org/src/scripts/pocketbase/Authentication.ts
2025-03-01 04:19:32 -08:00

195 lines
5.2 KiB
TypeScript

import PocketBase from "pocketbase";
import yaml from "js-yaml";
import configYaml from "../../config/pocketbaseConfig.yml?raw";
// Configuration type definitions
interface Config {
api: {
baseUrl: string;
oauth2: {
redirectPath: string;
providerName: string;
};
};
}
// Parse YAML configuration
const config = yaml.load(configYaml) as Config;
export class Authentication {
private pb: PocketBase;
private static instance: Authentication;
private authChangeCallbacks: ((isValid: boolean) => void)[] = [];
private isUpdating: boolean = false;
private authSyncServiceInitialized: boolean = false;
private constructor() {
// Use the baseUrl from the config file
this.pb = new PocketBase(config.api.baseUrl);
// Configure PocketBase client
this.pb.autoCancellation(false); // Disable auto-cancellation globally
// Listen for auth state changes
this.pb.authStore.onChange(() => {
if (!this.isUpdating) {
this.notifyAuthChange();
}
});
}
/**
* Get the singleton instance of Authentication
*/
public static getInstance(): Authentication {
if (!Authentication.instance) {
Authentication.instance = new Authentication();
}
return Authentication.instance;
}
/**
* Get the PocketBase instance
*/
public getPocketBase(): PocketBase {
return this.pb;
}
/**
* Handle user login through OAuth2
*/
public async login(): Promise<void> {
try {
const authMethods = await this.pb.collection("users").listAuthMethods();
const oidcProvider = authMethods.oauth2?.providers?.find(
(p: { name: string }) => p.name === config.api.oauth2.providerName,
);
if (!oidcProvider) {
throw new Error("OIDC provider not found");
}
localStorage.setItem("provider", JSON.stringify(oidcProvider));
const redirectUrl =
window.location.origin + config.api.oauth2.redirectPath;
const authUrl = oidcProvider.authURL + encodeURIComponent(redirectUrl);
window.location.href = authUrl;
} catch (err) {
console.error("Authentication error:", err);
throw err;
}
}
/**
* Handle user logout
*/
public async logout(): Promise<void> {
try {
// Initialize AuthSyncService if needed (lazy loading)
await this.initAuthSyncService();
// Get AuthSyncService instance
const { AuthSyncService } = await import('../database/AuthSyncService');
const authSync = AuthSyncService.getInstance();
// Handle data cleanup before actual logout
await authSync.handleLogout();
// Clear auth store
this.pb.authStore.clear();
console.log('Logout completed successfully with data cleanup');
} catch (error) {
console.error('Error during logout:', error);
// Fallback to basic logout if sync fails
this.pb.authStore.clear();
}
}
/**
* Check if user is currently authenticated
*/
public isAuthenticated(): boolean {
return this.pb.authStore.isValid;
}
/**
* Get current user model
*/
public getCurrentUser(): any {
return this.pb.authStore.model;
}
/**
* Get current user ID
*/
public getUserId(): string | null {
return this.pb.authStore.model?.id || null;
}
/**
* Subscribe to auth state changes
* @param callback Function to call when auth state changes
*/
public onAuthStateChange(callback: (isValid: boolean) => void): void {
this.authChangeCallbacks.push(callback);
// Initialize AuthSyncService when first callback is registered
if (!this.authSyncServiceInitialized && this.authChangeCallbacks.length === 1) {
this.initAuthSyncService();
}
}
/**
* Remove auth state change subscription
* @param callback Function to remove from subscribers
*/
public offAuthStateChange(callback: (isValid: boolean) => void): void {
this.authChangeCallbacks = this.authChangeCallbacks.filter(
(cb) => cb !== callback,
);
}
/**
* Set updating state to prevent auth change notifications during updates
*/
public setUpdating(updating: boolean): void {
this.isUpdating = updating;
}
/**
* Notify all subscribers of auth state change
*/
private notifyAuthChange(): void {
const isValid = this.pb.authStore.isValid;
this.authChangeCallbacks.forEach((callback) => callback(isValid));
}
/**
* Initialize the AuthSyncService (lazy loading)
*/
private async initAuthSyncService(): Promise<void> {
if (this.authSyncServiceInitialized) return;
try {
// Dynamically import AuthSyncService to avoid circular dependencies
const { AuthSyncService } = await import('../database/AuthSyncService');
// Initialize the service
AuthSyncService.getInstance();
this.authSyncServiceInitialized = true;
console.log('AuthSyncService initialized successfully');
// If user is already authenticated, trigger initial sync
if (this.isAuthenticated()) {
const authSync = AuthSyncService.getInstance();
authSync.handleLogin().catch(err => {
console.error('Error during initial data sync:', err);
});
}
} catch (error) {
console.error('Failed to initialize AuthSyncService:', error);
}
}
}