/** * Job-Info Auth Flow - Standalone * Complete auth lifecycle: acquire tokens, refresh, persist, retrieve * No external imports - loads PocketBase from global scope */ class JobInfoAuthFlow { constructor() { this.pb = null; this.tokens = {}; this.init(); } /** * Initialize: load persisted tokens, check if active, prompt login if needed */ async init() { console.log('[JobInfoAuth] init() called'); // PocketBase is loaded via script tag if (typeof PocketBase === 'undefined') { throw new Error('PocketBase SDK not loaded. Check script tag in HTML.'); } this.pb = new PocketBase('http://localhost:8090'); console.log('[JobInfoAuth] PocketBase initialized:', this.pb); // Load tokens from localStorage this.loadTokens(); console.log('[JobInfoAuth] Tokens loaded from storage:', Object.keys(this.tokens)); // Check if tokens are active const hasActiveTokens = this.isTokenActive('pb-user') || this.isTokenActive('graph-user'); if (!hasActiveTokens) { console.log('[JobInfoAuth] No active tokens, prompting login...'); await this.promptLogin(); } else { console.log('[JobInfoAuth] Tokens active, refreshing...'); await this.refreshAllTokens(); } console.log('[JobInfoAuth] init() complete'); } /** * Prompt user to log in via OAuth popup */ async promptLogin() { console.log('[JobInfoAuth] promptLogin() called'); try { const authData = await this.pb.collection('users').authWithOAuth2({ provider: 'microsoft' }); console.log('[JobInfoAuth] OAuth successful, authData:', authData); // Store PocketBase user token const pbToken = authData.token; const displayName = authData.record?.name || authData.record?.email || 'Unknown'; this.storeToken('pb-user', pbToken, displayName); console.log('[JobInfoAuth] pb-user token stored'); // Extract and store Graph token from OAuth metadata const graphToken = this.extractGraphToken(authData.meta || {}); if (graphToken) { this.storeToken('graph-user', graphToken, displayName); console.log('[JobInfoAuth] graph-user token stored'); } else { console.log('[JobInfoAuth] No graph token in OAuth response'); } } catch (err) { console.error('[JobInfoAuth] OAuth failed:', err); throw new Error(`Login failed: ${err.message}`); } } /** * Extract Graph token from OAuth metadata */ extractGraphToken(metadata) { console.log('[JobInfoAuth] extractGraphToken() called, metadata:', metadata); if (!metadata) return null; // Microsoft OAuth response includes access_token for Graph if (metadata.accessToken) { return metadata.accessToken; } if (metadata.access_token) { return metadata.access_token; } console.log('[JobInfoAuth] No Graph token found in metadata'); return null; } /** * Refresh all tokens - keep them fresh, only replace pb-user on new token */ async refreshAllTokens() { console.log('[JobInfoAuth] refreshAllTokens() called'); try { // Refresh pb-user token if (this.tokens['pb-user']) { const oldToken = this.tokens['pb-user'].value; // Use PocketBase refresh const authData = await this.pb.collection('users').authRefresh(); const newToken = authData.token; // Only replace if genuinely new if (newToken && newToken !== oldToken) { console.log('[JobInfoAuth] pb-user token refreshed (new token)'); const displayName = this.tokens['pb-user'].displayName; this.storeToken('pb-user', newToken, displayName); } else { console.log('[JobInfoAuth] pb-user token still valid, not replacing'); } } // Graph token: don't require for refresh, but use if available if (this.tokens['graph-user']) { console.log('[JobInfoAuth] graph-user token exists, keeping it'); // Optionally refresh graph token here if needed } } catch (err) { console.warn('[JobInfoAuth] Token refresh had issues:', err.message); // Don't throw - tokens might still be valid } } /** * Store token with metadata */ storeToken(type, token, displayName) { console.log('[JobInfoAuth] storeToken():', type); const lastFourDigits = token.slice(-4); const decoded = this.decodeToken(token); const expiresAt = decoded?.exp ? decoded.exp * 1000 : Date.now() + (3600 * 1000); // Default 1 hour this.tokens[type] = { value: token, displayName, lastFourDigits, expiresAt, storedAt: Date.now() }; // Persist to localStorage localStorage.setItem(`job-info-${type}`, JSON.stringify(this.tokens[type])); console.log(`[JobInfoAuth] ${type} stored, expires at:`, new Date(expiresAt).toISOString()); } /** * Load tokens from localStorage */ loadTokens() { console.log('[JobInfoAuth] loadTokens() called'); const types = ['pb-user', 'graph-user', 'pb-agent', 'graph-agent']; types.forEach(type => { const stored = localStorage.getItem(`job-info-${type}`); if (stored) { try { this.tokens[type] = JSON.parse(stored); console.log(`[JobInfoAuth] Loaded ${type} from localStorage`); } catch (err) { console.warn(`[JobInfoAuth] Failed to parse ${type}:`, err); localStorage.removeItem(`job-info-${type}`); } } }); } /** * Check if token is active (exists and not expired) */ isTokenActive(type) { const token = this.tokens[type]; if (!token) { console.log(`[JobInfoAuth] isTokenActive(${type}): no token stored`); return false; } const isExpired = token.expiresAt && Date.now() > token.expiresAt; if (isExpired) { console.log(`[JobInfoAuth] isTokenActive(${type}): expired`); return false; } console.log(`[JobInfoAuth] isTokenActive(${type}): active`); return true; } /** * Get current auth state */ getState() { return { 'pb-user': this.tokens['pb-user'] || null, 'graph-user': this.tokens['graph-user'] || null, 'pb-agent': this.tokens['pb-agent'] || null, 'graph-agent': this.tokens['graph-agent'] || null, userDisplayName: this.tokens['pb-user']?.displayName || 'Unknown' }; } /** * Logout - clear all tokens */ logout() { console.log('[JobInfoAuth] logout() called'); const types = ['pb-user', 'graph-user', 'pb-agent', 'graph-agent']; types.forEach(type => { localStorage.removeItem(`job-info-${type}`); }); this.tokens = {}; if (this.pb) { this.pb.authStore.clear(); } console.log('[JobInfoAuth] All tokens cleared'); } /** * Decode JWT token to get exp claim */ decodeToken(token) { try { const parts = token.split('.'); if (parts.length !== 3) return null; const decoded = JSON.parse(atob(parts[1])); return decoded; } catch (err) { console.warn('[JobInfoAuth] Failed to decode token:', err); return null; } } } // Create singleton instance const JobInfoAuth = new JobInfoAuthFlow(); // Expose globally if (typeof window !== 'undefined') { window.JobInfoAuth = JobInfoAuth; }