import PocketBase from 'pocketbase'; import { AuthConfig, AuthState, AuthCallbacks } from './types'; /** * PocketBase OAuth2 Frontend Module * Handles user authentication and token management */ export class PocketBaseAuth { private pb: PocketBase; private config: Required; private callbacks: AuthCallbacks; private state: AuthState; constructor(config: AuthConfig, callbacks?: Partial) { this.config = { pbUrl: config.pbUrl || '', collection: config.collection || 'Users', provider: config.provider || 'microsoft', loginContainerId: config.loginContainerId || 'loginContainer', userDisplayNameId: config.userDisplayNameId || 'userDisplayName', userEmailId: config.userEmailId || 'userEmailValue', loginBtnId: config.loginBtnId || 'loginBtn', loginErrorId: config.loginErrorId || 'loginError', }; this.callbacks = { onAuthSuccess: callbacks?.onAuthSuccess || (() => {}), onAuthFailure: callbacks?.onAuthFailure || (() => {}), onTokenUpdate: callbacks?.onTokenUpdate || (() => {}), onUiUpdate: callbacks?.onUiUpdate || (() => {}), }; this.pb = new PocketBase(this.config.pbUrl); this.state = { isAuthenticated: false, user: null, token: null, }; this.init(); } /** * Initialize auth module */ private init(): void { this.updateAuthUI(); if (this.pb.authStore.isValid && this.pb.authStore.token) { this.ensureUserLogged(); } this.setupLoginButton(); } /** * Setup login button click handler */ private setupLoginButton(): void { const loginBtn = document.getElementById(this.config.loginBtnId); if (!loginBtn) { console.warn(`Login button with id "${this.config.loginBtnId}" not found`); return; } loginBtn.addEventListener('click', async (e) => { e.preventDefault(); await this.handleLogin(); }); } /** * Handle login button click */ private async handleLogin(): Promise { const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement; const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement; if (!loginBtn || !loginError) return; loginBtn.disabled = true; loginBtn.textContent = 'Checking session...'; loginError.classList.add('hidden'); try { // Try to use existing token first const hadToken = await this.ensureUserLogged(); if (hadToken) { this.resetLoginButton(); return; } // Otherwise perform OAuth loginBtn.textContent = 'Logging in...'; const authData = await this.pb.collection(this.config.collection).authWithOAuth2({ provider: this.config.provider, }); this.updateState(authData); this.updateAuthUI(); this.callbacks.onAuthSuccess?.(this.state); console.log('✓ Logged in with', this.config.provider); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; console.error('Login failed:', message); loginError.textContent = `Login failed: ${message}`; loginError.classList.remove('hidden'); this.callbacks.onAuthFailure?.(error); } finally { this.resetLoginButton(); } } /** * Ensure user is logged in (check existing token) */ async ensureUserLogged(): Promise { if (!this.pb.authStore.isValid || !this.pb.authStore.token) { return false; } try { const refresh = await this.pb.collection(this.config.collection).authRefresh(); this.updateState(refresh); this.updateAuthUI(); this.callbacks.onTokenUpdate?.(this.state); console.log('✓ Token refreshed'); return true; } catch (error) { console.warn('Existing token invalid, clearing auth'); this.pb.authStore.clear(); this.updateState(null); this.updateAuthUI(); return false; } } /** * Update internal auth state */ private updateState(data: any): void { if (!data) { this.state = { isAuthenticated: false, user: null, token: null, }; return; } const record = data.record || this.pb.authStore.record || this.pb.authStore.model; const meta = data.meta || {}; const model = this.pb.authStore.model; this.state = { isAuthenticated: true, user: { id: record?.id || model?.id || 'unknown', name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User', email: record?.email || model?.email || meta?.email || '(no email)', }, token: this.pb.authStore.token, }; } /** * Update UI based on auth state */ updateAuthUI(): void { const loginContainer = document.getElementById(this.config.loginContainerId); const userDisplayName = document.getElementById(this.config.userDisplayNameId); const userEmail = document.getElementById(this.config.userEmailId); if (!loginContainer) return; if (this.pb.authStore.isValid) { loginContainer.classList.add('hidden'); if (this.state.user) { if (userDisplayName) userDisplayName.textContent = this.state.user.name; if (userEmail) userEmail.textContent = this.state.user.email; } } else { loginContainer.classList.remove('hidden'); const loginError = document.getElementById(this.config.loginErrorId); if (loginError) loginError.classList.add('hidden'); } this.callbacks.onUiUpdate?.(this.state); } /** * Check token status (for verification/display) */ async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> { const pbToken = !!this.pb.authStore.token; return { pbToken }; } /** * Get current auth state */ getAuthState(): AuthState { return { ...this.state }; } /** * Get PocketBase instance (for direct usage if needed) */ getPocketBase(): PocketBase { return this.pb; } /** * Logout */ logout(): void { this.pb.authStore.clear(); this.updateState(null); this.updateAuthUI(); console.log('✓ Logged out'); } /** * Reset login button to initial state */ private resetLoginButton(): void { const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement; if (loginBtn) { loginBtn.disabled = false; loginBtn.textContent = 'Login with Microsoft'; } } } /** * Quick init function for simple use cases */ export async function initPocketBaseAuth(config: AuthConfig): Promise { const auth = new PocketBaseAuth(config); return auth; }