1/** 2 * Firebase Client Configuration 3 * 4 * This config is safe to expose publicly - Firebase security is handled by: 5 * 1. Firebase Security Rules 6 * 2. Domain restrictions in Firebase Console 7 * 3. Server-side token verification 8 * 9 * Auto-initializes on script load. Use `await FirebaseAuth.ready` to wait for auth. 10 * Includes automatic token refresh via Firebase SDK and visibility change handling. 11 */ 12 13// Firebase configuration - loaded from environment via backend 14let firebaseConfig = null; 15let firebaseInitialized = false; 16let refreshIntervalId = null; 17let visibilityHandlerAdded = false; 18 19// Promise that resolves when Firebase auth state is ready 20// Components can await this before making authenticated API calls 21let authReadyResolve; 22const authReadyPromise = new Promise(resolve => { 23 authReadyResolve = resolve; 24}); 25 26/** 27 * Initialize Firebase with config from backend 28 * Waits for auth state to be restored before returning 29 */ 30async function initializeFirebaseClient() { 31 if (firebaseInitialized) { 32 return firebase; 33 } 34 35 try { 36 // Fetch config from backend (keeps API key out of static files) 37 const response = await fetch('/api/firebase-config'); 38 if (!response.ok) { 39 throw new Error('Failed to load Firebase config'); 40 } 41 42 firebaseConfig = await response.json(); 43 44 if (!firebaseConfig.apiKey) { 45 console.warn('Firebase not configured - using legacy auth'); 46 authReadyResolve(null); 47 return null; 48 } 49 50 firebase.initializeApp(firebaseConfig); 51 firebaseInitialized = true; 52 53 // Wait for auth state to be restored (important for page reloads) 54 firebase.auth().onAuthStateChanged(async (user) => { 55 if (user) { 56 // Store initial token 57 try { 58 const token = await user.getIdToken(); 59 storeToken(token); 60 61 // Set up proactive token refresh (every 45 minutes) 62 setupTokenRefresh(); 63 64 // Add visibility handler for tab wake-up (only once) 65 if (!visibilityHandlerAdded) { 66 addVisibilityHandler(); 67 visibilityHandlerAdded = true; 68 } 69 } catch (error) { 70 console.error('Failed to get initial token:', error); 71 } 72 } else { 73 // User signed out - clear token and refresh interval 74 clearStoredToken(); 75 clearRefreshInterval(); 76 } 77 78 authReadyResolve(user); 79 }); 80 81 return firebase; 82 } catch (error) { 83 console.error('Failed to initialize Firebase:', error); 84 authReadyResolve(null); 85 return null; 86 } 87} 88 89/** 90 * Get the current Firebase user 91 */ 92function getCurrentUser() { 93 return firebase.auth?.().currentUser || null; 94} 95 96// Token refresh state - prevents concurrent refresh attempts 97let tokenRefreshPromise = null; 98 99/** 100 * Get Firebase ID token for API calls 101 * Automatically waits for auth to be ready before getting token 102 * Proactively refreshes if token is expired or about to expire 103 * Uses mutex pattern to prevent concurrent refresh attempts 104 */ 105async function getFirebaseToken() { 106 // Wait for Firebase auth to be ready (auto-initialized on page load) 107 await authReadyPromise; 108 109 const user = getCurrentUser(); 110 if (!user) return null; 111 112 // Check if stored token is expired or expiring soon (within 5 minutes) 113 const storedExpiry = localStorage.getItem('authTokenExpiry'); 114 const expiryTime = storedExpiry ? parseInt(storedExpiry, 10) : 0; 115 const now = Date.now(); 116 const fiveMinutes = 5 * 60 * 1000; 117 const needsRefresh = expiryTime === 0 || (expiryTime - now) < fiveMinutes; 118 119 // If refresh needed, use mutex to prevent concurrent refreshes 120 if (needsRefresh) { 121 if (!tokenRefreshPromise) { 122 tokenRefreshPromise = (async () => { 123 try { 124 const token = await user.getIdToken(true); 125 storeToken(token); 126 return token; 127 } finally { 128 tokenRefreshPromise = null; 129 } 130 })(); 131 } 132 return tokenRefreshPromise; 133 } 134 135 // Token is still valid, use Firebase SDK's cached token 136 try { 137 return await user.getIdToken(); 138 } catch (error) { 139 // Token fetch failed - try force refresh once 140 if (!tokenRefreshPromise) { 141 tokenRefreshPromise = (async () => { 142 try { 143 const token = await user.getIdToken(true); 144 storeToken(token); 145 return token; 146 } catch (refreshError) { 147 // Session may be completely invalid - clear state 148 clearStoredToken(); 149 return null; 150 } finally { 151 tokenRefreshPromise = null; 152 } 153 })(); 154 } 155 return tokenRefreshPromise; 156 } 157} 158 159// Auto-initialize Firebase when script loads (non-blocking) 160// This starts the initialization process immediately 161initializeFirebaseClient(); 162 163/** 164 * Sign in with email and password 165 */ 166async function firebaseSignIn(email, password) { 167 if (!firebaseInitialized) { 168 throw new Error('Firebase not initialized'); 169 } 170 171 return firebase.auth().signInWithEmailAndPassword(email, password); 172} 173 174/** 175 * Sign out 176 */ 177async function firebaseSignOut() { 178 if (!firebaseInitialized) { 179 return; 180 } 181 182 return firebase.auth().signOut(); 183} 184 185/** 186 * Send password reset email with custom action URL 187 */ 188async function firebaseSendPasswordReset(email) { 189 if (!firebaseInitialized) { 190 throw new Error('Firebase not initialized'); 191 } 192 193 // Use custom action URL to redirect to our branded password setup page 194 const actionCodeSettings = { 195 url: `${window.location.origin}/set-password.html`, 196 handleCodeInApp: false 197 }; 198 199 return firebase.auth().sendPasswordResetEmail(email, actionCodeSettings); 200} 201 202/** 203 * Create user with email and password 204 */ 205async function firebaseCreateUser(email, password) { 206 if (!firebaseInitialized) { 207 throw new Error('Firebase not initialized'); 208 } 209 210 return firebase.auth().createUserWithEmailAndPassword(email, password); 211} 212 213/** 214 * Listen for auth state changes 215 */ 216function onAuthStateChanged(callback) { 217 if (!firebaseInitialized) { 218 return () => {}; 219 } 220 221 return firebase.auth().onAuthStateChanged(callback); 222} 223 224// ========================================== 225// API Guardrail Notification 226// ========================================== 227 228/** 229 * Show guardrail notification banner for circuit breaker / rate limit errors 230 * @param {string}
230 code - Error code (CIRCUIT_OPEN, RATE_LIMITED, ERROR_RATE_EXCEEDED) 231 * @param {number} retryAfter - Seconds until retry is allowed 232 * @param {string} platform - Platform name 233 */ 234function showGuardrailNotification(code, retryAfter, platform = 'API') { 235 // Remove existing notification if any 236 const existing = document.querySelector('.guardrail-notification'); 237 if (existing) existing.remove(); 238 239 let title, message, icon; 240 switch (code) { 241 case 'CIRCUIT_OPEN': 242 title = `${platform} Temporarily Paused`; 243 message = `We've detected repeated errors from ${platform} and temporarily paused requests to protect your account.`; 244 icon = 'ð¡ï¸'; 245 break; 246 case 'RATE_LIMITED': 247 title = `${platform} Rate Limit Reached`; 248 message = `You've made too many requests. ${platform} requires a brief cooldown.`; 249 icon = 'â±ï¸'; 250 break; 251 case 'ERROR_RATE_EXCEEDED': 252 title = `High Error Rate Detected`; 253 message = `Multiple ${platform} errors occurred. Taking a brief pause to recover.`; 254 icon = 'â ï¸'; 255 break; 256 default: 257 title = `${platform} Issue`; 258 message = `Temporary issue with ${platform}.`; 259 icon = 'â¹ï¸'; 260 } 261 262 const notification = document.createElement('div'); 263 notification.className = 'guardrail-notification'; 264 notification.setAttribute('role', 'alert'); 265 notification.setAttribute('aria-live', 'assertive'); 266 notification.setAttribute('aria-atomic', 'true'); 267 notification.innerHTML = ` 268 <div class="guardrail-notification-content"> 269 <div class="guardrail-notification-icon">${icon}</div> 270 <div class="guardrail-notification-text"> 271 <div class="guardrail-notification-title">${title}</div> 272 <div class="guardrail-notification-message">${message}</div> 273 ${retryAfter ? ` 274 <div class="guardrail-notification-countdown"> 275 Ready to retry in: <span class="countdown-timer">${retryAfter}</span> seconds 276 </div> 277 ` : ''} 278 <div class="guardrail-notification-footer"> 279 <span class="guardrail-help-text">Need help?</span> 280 <button class="guardrail-contact-btn" onclick="if(window.Tawk_API){Tawk_API.maximize();}"> 281 ð¬ Contact Support 282 </button> 283 </div> 284 </div> 285 <button class="guardrail-notification-close" aria-label="Close notification">Ã</button> 286 </div> 287 `; 288 289 // Add close button handler that clears timer 290 const closeBtn = notification.querySelector('.guardrail-notification-close'); 291 closeBtn.addEventListener('click', () => { 292 if (notification._timerId) clearInterval(notification._timerId); 293 notification.remove(); 294 }); 295 296 // Add styles if not already present 297 if (!document.querySelector('#guardrail-notification-styles')) { 298 const styles = document.createElement('style'); 299 styles.id = 'guardrail-notification-styles'; 300 styles.textContent = ` 301 .guardrail-notification { 302 position: fixed; 303 top: 20px; 304 right: 20px; 305 z-index: 10000; 306 background: #ffffff; 307 border: 1px solid #e5e7eb; 308 border-left: 4px solid #6366f1; 309 border-radius: 12px; 310 box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04); 311 padding: 16px 20px; 312 max-width: 400px; 313 animation: slideIn 0.3s ease-out; 314 } 315 @keyframes slideIn { 316 from { transform: translateX(20px); opacity: 0; } 317 to { transform: translateX(0); opacity: 1; } 318 } 319 .guardrail-notification-content { 320 display: flex; 321 align-items: flex-start; 322 gap: 12px; 323 } 324 .guardrail-notification-icon { 325 font-size: 28px; 326 flex-shrink: 0; 327 } 328 .guardrail-notification-text { 329 flex: 1; 330 } 331 .guardrail-notification-title { 332 font-size: 16px; 333 font-weight: 600; 334 color: #111827; 335 margin-bottom: 4px; 336 } 337 .guardrail-notification-message { 338 font-size: 14px; 339 color: #6b7280; 340 line-height: 1.4; 341 } 342 .guardrail-notification-countdown { 343 margin-top: 10px; 344 font-size: 14px; 345 color: #6366f1; 346 font-weight: 500; 347 } 348 .countdown-timer { 349 font-family: monospace; 350 font-size: 16px; 351 background: rgba(99, 102, 241, 0.1); 352 padding: 2px 8px; 353 border-radius: 4px; 354 color: #4f46e5; 355 } 356 .guardrail-notification-close { 357 background: none; 358 border: none; 359 color: #9ca3af; 360 font-size: 24px; 361 cursor: pointer; 362 padding: 0; 363 line-height: 1; 364 transition: color 0.2s; 365 } 366 .guardrail-notification-close:hover { 367 color: #374151; 368 } 369 .guardrail-notification-footer { 370 margin-top: 12px; 371 padding-top: 12px; 372 border-top: 1px solid #e5e7eb; 373 display: flex; 374 align-items: center; 375 gap: 10px; 376 } 377 .guardrail-help-text { 378 font-size: 13px; 379 color: #6b7280; 380 } 381 .guardrail-contact-btn { 382 background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); 383 border: none; 384 color: white; 385 padding: 8px 16px; 386 border-radius: 6px; 387 font-size: 13px; 388 font-weight: 500; 389 cursor: pointer; 390 transition: all 0.2s; 391 display: inline-flex; 392 align-items: center; 393 justify-content: center; 394 gap: 6px; 395 line-height: 1; 396 vertical-align: middle; 397 } 398 .guardrail-contact-btn:hover { 399 background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%); 400 transform: translateY(-1px); 401 } 402 @media (max-width: 440px) { 403 .guardrail-notification { 404 right: 10px; 405 left: 10px; 406 max-width: none; 407 } 408 } 409 `; 410 document.head.appendChild(styles); 411 } 412 413 document.body.appendChild(notification); 414 415 // Start countdown timer if retryAfter is provided 416 if (retryAfter && retryAfter > 0) { 417 let remaining = retryAfter; 418 notification._timerId = setInterval(() => { 419 remaining--; 420 const timerEl = notification.querySelector('.countdown-timer'); 421 if (timerEl) { 422 timerEl.textContent = remaining; 423 } 424 if (remaining <= 0) { 425 clearInterval(notification._timerId); 426 notification._timerId = null; 427 const countdownEl = notification.querySelector('.guardrail-notification-countdown'); 428 if (countdownEl) { 429 countdownEl.innerHTML = '<span style="color: #10b981;">â Ready to retry</span>'; 430 } 431 // Auto-remove after 3 more seconds 432 setTimeout(() => { 433 if (notification._timerId) clearInterval(notification._timerId); 434 notification.remove(); 435 }, 3000); 436 } 437 }, 1000); 438 } else {
439 // Auto-remove after 10 seconds if no countdown 440 setTimeout(() => { 441 if (notification._timerId) clearInterval(notification._timerId); 442 notification.remove(); 443 }, 10000); 444 } 445} 446 447/** 448 * Show error notification for non-blocking errors (Meta API errors, deployment failures, etc.) 449 * @param {string} title - Error title 450 * @param {string} message - Error message 451 * @param {string} type - 'error' | 'warning' | 'info' | 'success' 452 */ 453function showErrorNotification(title, message, type = 'error') { 454 // Remove existing notification if any 455 const existing = document.querySelector('.error-notification'); 456 if (existing) existing.remove(); 457 458 const config = { 459 error: { icon: 'â', color: '#ef4444', showSupport: true }, 460 warning: { icon: 'â ï¸', color: '#f59e0b', showSupport: false }, 461 info: { icon: 'â¹ï¸', color: '#3b82f6', showSupport: false }, 462 success: { icon: 'â ', color: '#10b981', showSupport: false } 463 }[type] || { icon: 'â', color: '#ef4444', showSupport: true }; 464 465 const notification = document.createElement('div'); 466 notification.className = 'error-notification'; 467 notification.setAttribute('role', type === 'error' ? 'alert' : 'status'); 468 notification.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite'); 469 notification.setAttribute('aria-atomic', 'true'); 470 notification.innerHTML = ` 471 <div class="error-notification-content"> 472 <div class="error-notification-icon" aria-hidden="true">${config.icon}</div> 473 <div class="error-notification-text"> 474 <div class="error-notification-title">${title}</div> 475 <div class="error-notification-message">${message}</div> 476 ${config.showSupport ? ` 477 <div class="error-notification-footer"> 478 <button class="error-contact-btn" onclick="if(window.Tawk_API){Tawk_API.maximize();}"> 479 ð¬ Contact Support 480 </button> 481 </div> 482 ` : ''} 483 </div> 484 <button class="error-notification-close" aria-label="Close notification">Ã</button> 485 </div> 486 `; 487 488 // Add styles if not already present 489 if (!document.querySelector('#error-notification-styles')) { 490 const styles = document.createElement('style'); 491 styles.id = 'error-notification-styles'; 492 styles.textContent = ` 493 .error-notification { 494 position: fixed; 495 top: 20px; 496 right: 20px; 497 z-index: 10000; 498 background: #ffffff; 499 border: 1px solid #e5e7eb; 500 border-left: 4px solid var(--error-color, #ef4444); 501 border-radius: 12px; 502 box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04); 503 padding: 16px 20px; 504 max-width: 400px; 505 animation: slideInError 0.3s ease-out; 506 } 507 @keyframes slideInError { 508 from { transform: translateX(20px); opacity: 0; } 509 to { transform: translateX(0); opacity: 1; } 510 } 511 .error-notification-content { 512 display: flex; 513 align-items: flex-start; 514 gap: 12px; 515 } 516 .error-notification-icon { 517 font-size: 24px; 518 flex-shrink: 0; 519 } 520 .error-notification-text { 521 flex: 1; 522 } 523 .error-notification-title { 524 font-size: 15px; 525 font-weight: 600; 526 color: #111827; 527 margin-bottom: 4px; 528 } 529 .error-notification-message { 530 font-size: 13px; 531 color: #6b7280; 532 line-height: 1.4; 533 } 534 .error-notification-close { 535 background: none; 536 border: none; 537 color: #9ca3af; 538 font-size: 24px; 539 cursor: pointer; 540 padding: 0; 541 line-height: 1; 542 transition: color 0.2s; 543 } 544 .error-notification-close:hover { 545 color: #374151; 546 } 547 .error-notification-footer { 548 margin-top: 10px; 549 } 550 .error-contact-btn { 551 background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); 552 border: none; 553 color: white; 554 padding: 6px 12px; 555 border-radius: 6px; 556 font-size: 12px; 557 font-weight: 500; 558 cursor: pointer; 559 transition: all 0.2s; 560 display: inline-flex; 561 align-items: center; 562 justify-content: center; 563 gap: 6px; 564 line-height: 1; 565 } 566 .error-contact-btn:hover { 567 background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%); 568 } 569 @media (max-width: 440px) { 570 .error-notification { 571 right: 10px; 572 left: 10px; 573 max-width: none; 574 } 575 } 576 `; 577 document.head.appendChild(styles); 578 } 579 580 // Set the accent color 581 notification.style.setProperty('--error-color', config.color); 582 583 document.body.appendChild(notification); 584 585 // Add close button handler 586 const closeBtn = notification.querySelector('.error-notification-close'); 587 closeBtn.addEventListener('click', () => notification.remove()); 588 589 // Auto-remove based on type (success is quick, errors need more reading time) 590 const timeouts = { success: 3000, info: 5000, warning: 6000, error: 8000 }; 591 setTimeout(() => notification.remove(), timeouts[type] || 5000); 592} 593 594// Export notification functions globally 595window.showGuardrailNotification = showGuardrailNotification; 596window.showErrorNotification = showErrorNotification; 597 598/** 599 * Make an authenticated API request 600 * Automatically includes Firebase token in Authorization header 601 * Auto-initializes Firebase on first use (once per session) 602 * Retries once with fresh token on 401 (handles token expiration) 603 * Redirects to login on persistent auth failure 604 * @param {string} url - API endpoint URL 605 * @param {object} options - Fetch options (method, body, etc.) 606 * @returns {Promise<Response>}
606 - Fetch response (parsed as JSON) 607 */ 608async function authenticatedFetch(url, options = {}, _isRetry = false) { 609 // Auto-initialize Firebase on first API call 610 if (!firebaseInitialized) { 611 await initializeFirebaseClient(); 612 } 613 614 // Use effective email (impersonated if active) 615 const effectiveEmail = window.AuthHelper?.getEffectiveUserEmail?.() || sessionStorage.getItem('userEmail') || ''; 616 617 // FormData bodies must NOT get a manual Content-Type â fetch() needs to 618 // generate its own multipart boundary. 619 const headers = { 620 'X-User-Email': effectiveEmail, 621 ...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), 622 ...options.headers 623 }; 624 625 // Get Firebase token for authentication (force refresh on retry) 626 let token; 627 if (_isRetry && firebase.auth?.().currentUser) { 628 // Force refresh on retry - bypass cache 629 try { 630 token = await firebase.auth().currentUser.getIdToken(true); 631 storeToken(token); 632 } catch (e) { 633 // Force refresh failed - session is invalid 634 clearStoredToken(); 635 token = null; 636 } 637 } else { 638 token = await getFirebaseToken(); 639 } 640 641 if (token) { 642 headers['Authorization'] = `Bearer ${token}`; 643 } 644 645 // Add impersonation audit header when impersonating 646 if (window.AuthHelper?.isImpersonating?.()) { 647 headers['X-Impersonated-By'] = window.AuthHelper.getRealAdminEmail(); 648 } 649 650 const response = await fetch(url, { 651 ...options, 652 headers 653 }); 654 655 // Handle 401 - token might be expired, retry once with fresh token 656 if (response.status === 401 && !_isRetry && firebase.auth?.().currentUser) { 657 return authenticatedFetch(url, options, true); 658 } 659 660 // Handle persistent 401 after retry - return error, let page handle redirect 661 if (response.status === 401 && _isRetry) { 662 clearStoredToken(); 663 try { 664 return await response.json(); 665 } catch (e) { 666 return { error: 'Authentication failed', code: 'AUTH_FAILED' }; 667 } 668 } 669 670 // Handle 429 - check for guardrail blocks (circuit breaker, rate limit) 671 if (response.status === 429) { 672 try { 673 const errorData = await response.clone().json(); 674 if (errorData.code === 'CIRCUIT_OPEN' || errorData.code === 'RATE_LIMITED' || errorData.code === 'ERROR_RATE_EXCEEDED') { 675 // Determine platform from URL 676 const platform = url.includes('/api/google') ? 'Google Ads' : url.includes('/api/meta') ? 'Meta' : 'API'; 677 showGuardrailNotification(errorData.code, errorData.retryAfter, platform); 678 return errorData; 679 } 680 } catch (e) { 681 // Ignore JSON parse errors 682 } 683 } 684 685 return response.json(); 686} 687 688/** 689 * Handle auth failure - call this from pages that need redirect on session expiry 690 * @param {Object} response - API response to check 691 * @returns {boolean} - true if redirected, false if no auth error 692 */ 693function handleAuthFailure(response) { 694 const authErrorCodes = ['UNAUTHORIZED', 'SESSION_EXPIRED', 'USER_NOT_REGISTERED', 'AUTH_FAILED']; 695 if (response?.code && authErrorCodes.includes(response.code)) { 696 sessionStorage.setItem('authRedirectReason', response.code); 697 window.location.href = '/index.html'; 698 return true; 699 } 700 return false; 701} 702 703// ========================================== 704// Token Refresh Helpers 705// ========================================== 706 707/** 708 * Store token in localStorage (for other modules to access) 709 */ 710function storeToken(token) { 711 localStorage.setItem('authToken', token); 712 localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString()); 713} 714 715/** 716 * Clear stored token 717 */ 718function clearStoredToken() { 719 localStorage.removeItem('authToken'); 720 localStorage.removeItem('authTokenExpiry'); 721} 722 723/** 724 * Set up proactive token refresh (every 45 minutes) 725 * Firebase SDK handles auto-refresh, but we add extra safety 726 * Uses currentUser at execution time (not closure) to handle sign-out/sign-in 727 */ 728function setupTokenRefresh() { 729 clearRefreshInterval(); 730 731 refreshIntervalId = setInterval(async () => { 732 // Always get fresh currentUser reference (handles multi-tab sign out) 733 const currentUser = firebase.auth?.().currentUser; 734 if (currentUser) {
735 try { 736 const token = await currentUser.getIdToken(); 737 storeToken(token); 738 } catch (e) { 739 // Silent fail - next API call will trigger refresh 740 } 741 } 742 }, 45 * 60 * 1000); // Every 45 minutes 743} 744 745/** 746 * Clear refresh interval 747 */ 748function clearRefreshInterval() { 749 if (refreshIntervalId) { 750 clearInterval(refreshIntervalId); 751 refreshIntervalId = null; 752 } 753} 754 755/** 756 * Add visibility change handler - refresh token when tab becomes visible 757 * Handles sleep/background scenarios with debounce to prevent rapid refreshes 758 */ 759let lastVisibilityRefresh = 0; 760function addVisibilityHandler() { 761 document.addEventListener('visibilitychange', async () => { 762 if (document.visibilityState === 'visible') { 763 const currentUser = firebase.auth?.().currentUser; 764 if (!currentUser) return; 765 766 // Debounce: only refresh if last refresh was > 30 seconds ago 767 const now = Date.now(); 768 if (now - lastVisibilityRefresh < 30000) return; 769 lastVisibilityRefresh = now; 770 771 try { 772 const token = await currentUser.getIdToken(true); // Force refresh 773 storeToken(token); 774 } catch (error) { 775 // Token refresh failed - clear stored token, next API call will handle 776 clearStoredToken(); 777 } 778 } 779 }); 780} 781 782/** 783 * Get stored token synchronously (for backward compatibility) 784 */ 785function getStoredToken() { 786 return localStorage.getItem('authToken'); 787} 788 789/** 790 * Check if user is authenticated 791 */ 792function isAuthenticated() { 793 return !!firebase.auth?.().currentUser; 794} 795 796// Export for use in other scripts 797window.FirebaseAuth = { 798 initialize: initializeFirebaseClient, 799 ready: authReadyPromise, // Await this to ensure auth is ready 800 getCurrentUser, 801 getToken: getFirebaseToken, // Auto-waits for auth to be ready 802 getStoredToken, // Synchronous access to stored token 803 isAuthenticated, // Check if user is logged in 804 signIn: firebaseSignIn, 805 signOut: firebaseSignOut, 806 sendPasswordReset: firebaseSendPasswordReset, 807 createUser: firebaseCreateUser, 808 onAuthStateChanged, 809 fetch: authenticatedFetch, 810 handleAuthFailure, // Check response and redirect if auth error 811 forceRefresh: async () => { 812 const user = firebase.auth?.().currentUser; 813 if (!user) return null; 814 const token = await user.getIdToken(true); 815 storeToken(token); 816 return token; 817 } 818}; 819 820// Convenient API client with shorthand methods 821window.api = { 822 fetch: authenticatedFetch, 823 getToken: getFirebaseToken, 824 handleAuthFailure, // Check response and redirect if auth error 825 826 async get(url) { 827 return authenticatedFetch(url); 828 }, 829 830 async post(url, data) { 831 return authenticatedFetch(url, { 832 method: 'POST', 833 body: JSON.stringify(data) 834 }); 835 }, 836 837 async put(url, data) { 838 return authenticatedFetch(url, { 839 method: 'PUT', 840 body: JSON.stringify(data) 841 }); 842 }, 843 844 async delete(url) { 845 return authenticatedFetch(url, { 846 method: 'DELETE' 847 }); 848 }, 849 850 // POST a FormData body (file uploads) â no JSON stringify, no Content-Type 851 // override (see authenticatedFetch() above for why). 852 async upload(url, formData) { 853 return authenticatedFetch(url, { 854 method: 'POST', 855 body: formData 856 }); 857 } 858};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.