PageSourceSearch

https://xpathlabs.ai/auth.js

js xpathlabs.ai collected 2026-10-05 13:20:31 UTC 20,717 bytes, 596 lines download raw bytes

1/**
2 * Authentication Module
3 *
4 * Uses Firebase Auth for secure authentication with fallback to legacy system.
5 * Supports:
6 * - Email/password login via Firebase
7 * - Password reset via Firebase
8 * - Role-based access with 6 roles:
9 *   - super_admin: Platform administrators (hardcoded)
10 *   - agency_owner: Creates brands, invites brand_user
11 *   - brand_owner: Upgraded from trial, invites brand_collaborator
12 *   - trial_user: Self-signup, limited trial period
13 *   - brand_user: Invited by agency_owner, configurable permissions
14 *   - brand_collaborator: Invited by brand_owner, configurable permissions
15 */
16
17// Super admin users (platform administrators)
18const SUPER_ADMIN_EMAILS = [
19  '[email protected]',
20  '[email protected]',
21  '[email protected]'
22];
23
24// Legacy admin users (for backward compatibility during transition)
25const LEGACY_ADMINS = [
26  { email: '[email protected]', password: 'Woods123!' },
27  { email: '[email protected]', password: 'Woods123!' },
28  { email: '[email protected]', password: 'Woods123!' }
29];
30
31let firebaseReady = false;
32
33// Initialize on page load
34document.addEventListener('DOMContentLoaded', async () => {
35  // Try to restore session from cookie (OAuth callback)
36  restoreSessionFromCookie();
37
38  // Initialize Firebase
39  try {
40    const fb = await window.FirebaseAuth?.initialize();
41    if (fb) {
42      firebaseReady = true;
43
44      // Listen for auth state changes
45      window.FirebaseAuth.onAuthStateChanged(async (user) => {
46        // Clear logout flag when auth state changes (user signed out or in)
47        const justLoggedOut = sessionStorage.getItem('justLoggedOut') === 'true';
48        if (justLoggedOut) {
49          sessionStorage.removeItem('justLoggedOut');
50        }
51
52        if (user) {
53
54          // Don't auto-login if user just logged out (flag was set before this callback)
55          if (justLoggedOut) {
56            return;
57          }
58
59          // User is signed in - check if session already exists
60          if (sessionStorage.getItem('isAuthenticated') !== 'true') {
61            await handleFirebaseUser(user);
62          }
63        }
64      });
65    }
66  } catch (error) {
67    console.warn('Firebase not available, using legacy auth:', error.message);
68  }
69
70  // Check if already authenticated
71  checkAuth();
72});
73
74/**
75 * Handle Firebase user sign-in
76 */
77async function handleFirebaseUser(user) {
78  const email = user.email.toLowerCase();
79
80  // Check if super admin
81  if (SUPER_ADMIN_EMAILS.includes(email)) {
82    // Store token for dashboard pages (avoids loading Firebase SDK on every page)
83    const token = await user.getIdToken();
84    localStorage.setItem('authToken', token);
85    localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString()); // 1 hour
86
87    sessionStorage.setItem('isAuthenticated', 'true');
88    sessionStorage.setItem('userEmail', email);
89    sessionStorage.setItem('userRole', 'super_admin');
90    sessionStorage.setItem('permissions', '');
91    redirectBasedOnRole('super_admin');
92    return;
93  }
94
95  // Get user role from MongoDB
96  try {
97    const token = await user.getIdToken();
98
99    // Store token for dashboard pages (avoids loading Firebase SDK on every page)
100    localStorage.setItem('authToken', token);
101    localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString()); // 1 hour
102
103    const response = await fetch('/api/auth/user-role', {
104      headers: {
105        'Authorization': `Bearer ${token}`,
106        'Content-Type': 'application/json'
107      }
108    });
109
110    let role = 'trial_user';
111    let permissions = null;
112    let userData = null;
113
114    if (response.ok) {
115      userData = await response.json();
116      role = userData.role || 'trial_user';
117      permissions = userData.permissions || null;
118    }
119
120    // Map permissions to new field names (support both old and new names from DB)
121    let mappedPermissions = null;
122    if (permissions) {
123      mappedPermissions = {
124        canAccessMetaAds: permissions.canAccessMetaAds ?? permissions.canViewMetrics ?? true,
125        canAccessCreativeStudio: permissions.canAccessCreativeStudio ?? permissions.canGenerateCreatives ?? true,
126        canAccessCreativePerformance: permissions.canAccessCreativePerformance ?? true,
127        canAccessBrandSettings: permissions.canAccessBrandSettings ?? permissions.canEditBrand ?? false,
128        canExecuteActions: permissions.canExecuteActions ?? permissions.canExecuteAIActions ?? true,
129        canAccessPhone: permissions.canAccessPhone ?? false,
130        canAccessSeoGeo: permissions.canAccessSeoGeo ?? false
131      };
132    }
133
134    // Store basic session info
135    sessionStorage.setItem('isAuthenticated', 'true');
136    sessionStorage.setItem('userEmail', email);
137    sessionStorage.setItem('userRole', role);
138    sessionStorage.setItem('permissions', mappedPermissions ? JSON.stringify(ma
138ppedPermissions) : '');
139
140    // Store access and subscription info for freemium model
141    if (userData?.accessVia) {
142      sessionStorage.setItem('accessVia', userData.accessVia);
143    }
144    if (userData?.invitedByRole) {
145      sessionStorage.setItem('invitedByRole', userData.invitedByRole);
146    }
147    if (userData?.subscription) {
148      sessionStorage.setItem('subscription', JSON.stringify(userData.subscription));
149    }
150    // Store hasFullAccess for immediate sidebar/page-access checks (avoids race condition)
151    if (userData?.hasFullAccess !== undefined) {
152      sessionStorage.setItem('hasFullAccess', userData.hasFullAccess ? 'true' : 'false');
153    }
154
155    // For single-brand users, fetch and cache their brand data
156    const isSingleBrandRole = !['super_admin', 'agency_owner'].includes(role);
157    if (isSingleBrandRole) {
158      try {
159        const brandsResponse = await fetch('/api/brands', {
160          headers: {
161            'Authorization': `Bearer ${token}`,
162            'Content-Type': 'application/json'
163          }
164        });
165        const brandsData = await brandsResponse.json();
166
167        if (brandsData.success && brandsData.brands && brandsData.brands.length > 0) {
168          const brand = brandsData.brands[0];
169          sessionStorage.setItem('userBrand', JSON.stringify(brand));
170        } else {
171          console.warn('No brands returned for user. Response:', brandsData);
172        }
173      } catch (brandError) {
174        console.warn('Could not load brand at login:', brandError.message);
175      }
176    }
177
178    // Redirect based on role
179    redirectBasedOnRole(role);
180  } catch (error) {
181    console.error('Error getting user role:', error);
182    // Default to trial_user role
183    sessionStorage.setItem('isAuthenticated', 'true');
184    sessionStorage.setItem('userEmail', email);
185    sessionStorage.setItem('userRole', 'trial_user');
186    redirectBasedOnRole('trial_user');
187  }
188}
189
190/**
191 * Redirect user based on their role and permissions
192 */
193function redirectBasedOnRole(role) {
194  // Owner roles go to main dashboard
195  if (['super_admin', 'agency_owner', 'brand_owner', 'trial_user'].includes(role)) {
196    window.location.href = '/meta-performance-manager.html';
197    return;
198  }
199
200  // For brand_user/brand_collaborator, redirect based on permissions
201  const permsStr = sessionStorage.getItem('permissions');
202  if (permsStr) {
203    try {
204      const perms = JSON.parse(permsStr);
205      // Redirect to first accessible page (Meta Ads first as it's always unlocked)
206      if (perms.canAccessMetaAds) {
207        window.location.href = '/meta-performance-manager.html';
208      } else if (perms.canAccessCreativeStudio) {
209        window.location.href = '/creative-studio.html';
210      } else if (perms.canAccessBrandSettings) {
211        window.location.href = '/brand.html';
212      } else {
213        // No page access - go to settings
214        window.location.href = '/settings.html';
215      }
216      return;
217    } catch (e) {
218      console.warn('Error parsing permissions:', e);
219    }
220  }
221
222  // Default fallback - Meta Ads Manager is always accessible
223  window.location.href = '/meta-performance-manager.html';
224}
225
226/**
227 * Restore session from OAuth callback cookie
228 */
229function restoreSessionFromCookie() {
230  try {
231    if (sessionStorage.getItem('isAuthenticated') === 'true') {
232      document.cookie = 'session_restore=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
233      return false;
234    }
235
236    const cookies = document.cookie.split(';');
237    for (const cookie of cookies) {
238      const [name, value] = cookie.trim().split('=');
239      if (name === 'session_restore' && value) {
240        const sessionData = JSON.parse(decodeURIComponent(value));
241        if (sessionData.email && sessionData.isAuthenticated) {
242          sessionStorage.setItem('isAuthenticated', 'true');
243          sessionStorage.setItem('userEmail', sessionData.email);
244          sessionStorage.setItem('userRole', sessionData.role || 'admin');
245          document.cookie = 'session_restore=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
246          return true;
247        }
248      }
249    }
250  } catch (e) {
251    console.error('Failed to restore session from cookie:', e);
252  }
253  return false;
254}
255
256/**
257 * Check if user is already authenticated
258 */
259function checkAuth() {
260  const isAuthenticated = sessionStorage.getItem('isAuthenticated');
261  if (isAuthenticated === 'true') {
262    // Verify auth token exists in localStorage (added after initial release)
263    // If session exists but token is missing, clear session and force re-login
264    const hasToken = window.AuthHelper?.hasValidToken?.() ?? localStorage.getItem('authToken');
265    if (!hasToken) {
266      console.warn('Session exists but auth token missing. Clearing session for re-login.');
267      sessionStorage.clear();
268      localStorage.removeItem('authToken');
269      localStorage.removeItem('authTokenExpiry');
270      // Don't redirect - let user login again
271      return;
272    }
273
274    const userRole = sessionStorage.getItem('userRole') || 'admin';
275    redirectBasedOnRole(userRole);
276  }
277}
278
279/**
280 * Login form handler
281 */
282document.getElementById('loginForm').addEventListener('submit', async function(e) {
283  e.preventDefault();
284
285  const email = document.getElementById('loginEmail').value.trim().toLowerCase();
286  const password = document.getElementById('loginPassword').value;
287
288  // Try Firebase Auth first
289  if (firebaseReady) {
290    try {
291      showMessage('Signing in...', 'info');
292      const userCredential = await window.FirebaseAuth.signIn(email, password);
293      showMessage('Login successful! Redirecting...', 'success');
294      // Handle login directly instead of relying on onAuthStateChanged
295      await handleFirebaseUser(userCredential.user);
296      return;
297    } catch (firebaseError) {
298
299      // Handle specific Firebase errors
300      if (firebaseError.code === 'auth/user-not-found' ||
301          firebaseError.code === 'auth/wrong-password' ||
302          firebaseError.code === 'auth/invalid-credential') {
303        // Try legacy auth for hardcoded admins
304      } else if (firebaseError.code === 'auth/too-many-requests') {
305        showMessage('Too many failed attempts. Please try again later.', 'error');
306        return;
307      } else {
308        // For other Firebase errors, still try legacy
309        console.warn('Firebase error:', firebaseError.code);
310      }
311    }
312  }
313
314  // Legacy authentication for super admins
315  const validUser = LEGACY_ADMINS.find(user =>
316    user.email.toLowerCase() === email && user.password === password
317  );
318
319  if (validUser) {
320    sessionStorage.setItem('isAuthenticated', 'true');
321    sessionStorage.setItem('userEmail', email);
322    sessionStorage.setItem('userRole', 'super_admin');
323    sessionStorage.setItem('permissions', '');
324
325    showMessage('Login successful! Redirecting...', 'success');
326    setTimeout(() => {
327      window.location.href = '/meta-performance-manager.html';
328    }, 1000);
329    return;
330  }
331
332  // Try MongoDB brand user login (legacy)
333  try {
334    showMessage('Checking credentials...', 'info');
335    const response = await fetch('/api/auth/login', {
336      method: 'POST',
337      headers: { 'Content-Type': 'application/json' },
338      body: JSON.stringify({ email, password })
339    });
340
341    const data = await response.json();
342
343    if (data.success) {
344      const role = data.user.role;
345      sessionStorage.setItem('isAuthenticated', 'true');
346      sessionStorage.setItem('userEmail', email);
347      sessionStorage.setItem('userRole', role);
348      sessionStorage.setItem('permissions', data.user.permissions ? JSON.stringify(data.user.permissions) : '');
349
350      // For single-brand users, cache their brand
351      if (data.user.brand) {
352        sessionStorage.setItem('userBrand', JSON.stringify(data.user.brand));
353      }
354
355      showMessage('Login successful! Redirecting...', 'success');
356      setTimeout(() => redirectBasedOnRole(role), 1000);
357    } else if (data.code === 'TRIAL_EXPIRED') {
358      showTrialExpiredModal(data.trialEndsAt);
359    } else {
360      showMessage(data.error || 'Invalid email or password', 'error');
361    }
362  } catch (error) {
363    console.error('Login error:', error);
364    showMessage('Invalid email or password', 'error');
365  }
366});
367
368/**
369 * Register form handler - Self-signup for freemium trial
370 */
371document.getElementById('registerForm').addEventListener('submit', async function(e) {
372  e.preventDefault();
373
374  const name = document.getElementById('registerName').value.trim();
375  const email = document.getElementById('registerEmail').value.trim().toLowerCase();
376  const company = document.getElementById('registerCompany').value.trim();
377  const accountType = document.getElementById('registerAccountType').value;
378  const website = document.getElementById('registerWebsite')?.value?.trim() || '';
379  const password = document.getElementById('registerPassword').value;
380  const adSpend = document.getElementById('registerAdSpend').value;
381
382  if (!name || !email || !company || !accountType || !password || !adSpend) {
383    showMessage('Please fill in all required fields', 'error');
384    return;
385  }
386
387  if (password.length < 6) {
388    showMessage('Password must be at least 6 characters', 'error');
389    return;
390  }
391
392  // Check if Firebase is ready
393  if (!firebaseReady) {
394    showMessage('Authentication system is loading. Please try again.', 'error');
395    return;
396  }
397
398  try {
399    showMessage('Creating your account...', 'info');
400
401    // Create Firebase user
402    const userCredential = await window.FirebaseAuth.createUser(email, password);
403    const user = userCredential.user;
404
405    // Get Firebase token
406    const idToken = await user.getIdToken();
407
408    // Register in MongoDB via API
409    const response = await fetch('/api/auth/register', {
410      method: 'POST',
411      headers: {
412        'Content-Type': 'application/json',
413        'Authorization': `Bearer ${idToken}`
414      },
415      body: JSON.stringify({
416        name,
417        company,
418        adSpend,
419        accountType,
420        website: website || null
421      })
422    });
423
424    const data = await response.json();
425
426    if (data.success) {
427      // Store auth token for dashboard pages
428      localStorage.setItem('authToken', idToken);
429      localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString()); // 1 hour
430
431      // Get role from API response (for existing users) or default to trial_user
432      const userRole = data.user?.role || 'trial_user';
433
434      // Store session info
435      sessionStorage.setItem('isAuthenticated', 'true');
436      sessionStorage.setItem('userEmail', email);
437      sessionStorage.setItem('userRole', userRole);
438      sessionStorage.setItem('permissions', '');
439
440      // Store brand ID if auto-created
441      if (data.user?.brandId) {
442        sessionStorage.setItem('currentBrandId', data.user.brandId);
443      }
444
445      const message = data.user?.isNew === false ? 'Welcome back! Redirecting...' : 'Account created! Redirecting...';
446      showMessage(message, 'success');
447      setTimeout(() => {
448        // Use consistent redirect logic based on role
449        redirectBasedOnRole(userRole);
450      }, 1000);
451    } else {
452      throw new Error(data.error || 'Registration failed');
453    }
454  } catch (error) {
455    console.error('Registration error:', error);
456
457    // Handle specific Firebase errors
458    let errorMessage = error.message;
459    if (error.code === 'auth/email-already-in-use') {
460      errorMessage = 'An account with this email already exists. Please sign in instead.';
461    } else if (error.code === 'auth/weak-password') {
462      errorMessage = 'Password is too weak. Please use at least 6 characters.';
463    } else if (error.code === 'auth/invalid-email') {
464      errorMessage = 'Invalid email address.';
465    }
466
467    showMessage(errorMessage, 'error');
468  }
469});
470
471/**
472 * Show trial expired modal
473 */
474function showTrialExpiredModal(trialEndsAt) {
475  const expiredDate = trialEndsAt ? new Date(trialEndsAt).toLocaleDateString() : 'recently';
476
477  let modal = document.getElementById('trialExpiredModal');
478  if (!modal) {
479    modal = document.createElement('div');
480    modal.id = 'trialExpiredModal';
481    modal.innerHTML = `
482      <div style="
483        position: fixed;
484        inset: 0;
485        background: rgba(0, 0, 0, 0.6);
486        backdrop-filter: blur(4px);
487        display: flex;
488        align-items: center;
489        justify-content: center;
490        z-index: 10000;
491      ">
492        <div style="
493          background: white;
494          border-radius: 16px;
495          padding: 2.5rem;
496          max-width: 420px;
497          text-align: center;
498          box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
499        ">
500          <div style="
501            width: 64px;
502            height: 64px;
503            background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
504            border-radius: 16px;
505            display: flex;
506            align-items: center;
507            justify-content: center;
508            margin: 0 auto 1.5rem;
509          ">
510            <i class="fas fa-clock" style="color: white; font-size: 1.75rem;"></i>
511          </div>
512          <h2 style="margin: 0 0 0.75rem; font-size: 1.5rem; color: #1f2937;">Your Trial Has Ended</h2>
513          <p style="margin: 0 0 0.5rem; color: #6b7280; font-size: 1rem; line-height: 1.6;">
514            Your free trial expired on <strong>${expiredDate}</strong>.
515          </p>
516          <p style="margin: 0 0 2rem; color: #6b7280; font-size: 1rem; line-height: 1.6;">
517            Upgrade now to continue using AI-powered campaign optimization.
518          </p>
519          <a href="https://calendly.com/conor-xpathlabs/meeting" target="_blank" rel="noopener noreferrer" style="
520            display: inline-flex;
521            align-items: center;
522            gap: 0.5rem;
523            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
524            color: white;
525            padding: 0.875rem 2rem;
526            border-radius: 10px;
527            font-weight: 600;
528            font-size: 1rem;
529            text-decoration: none;
530            box-shadow: 0 4px 14px rgba(102, 126, 234, 0.4);
531          ">
532            <i class="fas fa-calendar-check"></i>
533            Book a Call to Upgrade
534          </a>
535          <p style="margin: 1.5rem 0 0; font-size: 0.875rem; color: #9ca3af;">
536            Questions? Email us at <a href="mailto:[email protected]" style="color: #667eea;">[email protected]</a>
537          </p>
538        </div>
539      </div>
540    `;
541    document.body.appendChild(modal);
542  }
543
544  modal.style.display = 'block';
545}
546
547/**
548 * Show message helper
549 */
550function showMessage(message, type) {
551  const existingMsg = document.querySelector('.auth-message');
552  if (existingMsg) {
553    existingMsg.remove();
554  }
555
556  const msgDiv = document.createElement('div');
557  msgDiv.className = `auth-message auth-message-${type}`;
558  msgDiv.innerHTML = `
559    <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
560    <span>${message}</span>
561  `;
562
563  const authCard = document.querySelector('.auth-card');
564  const authForm = document.querySelector('.auth-form.active');
565  authCard.insertBefore(msgDiv, authForm);
566
567  setTimeout(() => {
568    msgDiv.remove();
569  }, 5000);
570}
571
572/**
573 * Tab switching between Login and Register forms
574 */
575document.querySelectorAll('.auth-tabs .tab-btn').forEach(btn => {
576  btn.addEventListener('click', function() {
577    const tab = this.dataset.tab;
578
579    // Update active tab button
580    document.querySelectorAll('.auth-tabs .tab-btn').forEach(b => b.classList.remove('active'));
581    this.classList.add('active');
582
583    // Show corresponding form
584    document.querySelectorAll('.auth-form').forEach(form => form.classList.remove('active'));
585    const targetForm = document.getElementById(tab === 'login' ? 'loginForm' : 'registerForm');
586    if (targetForm) {
587      targetForm.classList.add('active');
588    }
589
590    // Clear any existing messages
591    const existingMsg = document.querySelector('.auth-message');
592    if (existingMsg) {
593      existingMsg.remove();
594    }
595  });
596});

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.