/** * Authentication Handler for StreetRacer Web Dashboard * Handles login, subscription/credit-based access, and session management * * Access model: * - PERMANENT_VIP_EMAILS: Full access always * - Subscribers (standard/premium/vip): Dashboard access included, but * Sessions tab (500 credits) and Route editor (1000 credits) cost extra * - Non-subscribers: Pay credits per month for dashboard tiers: * Basic (1000cr) = Stats + Rankings * Extended (1500cr) = + Sessions * Full (2500cr) = All features * Sessions tab (500cr) and Route editor (1000cr) cost extra on top * * Firebase structure: * dashboardAccess/{uid}: { tier, activatedAt, expiresAt, creditsPaid } * dashboardFeatures/{uid}/{featureId}: { unlockedAt, creditsPaid } */ // Admin/VIP accounts that always have full access const PERMANENT_VIP_EMAILS = [ 'francois.reinert@me.com', 'leonard.reinert@me.com' ]; // Dashboard access tiers for non-subscribers const DASHBOARD_TIERS = { basic: { name: 'Basis', cost: 1000, tabs: ['statistics', 'rankings'] }, extended: { name: 'Erweitert', cost: 1500, tabs: ['statistics', 'rankings', 'sessions'] }, full: { name: 'Komplett', cost: 2500, tabs: ['statistics', 'rankings', 'sessions', 'track-editor'] }, }; // Per-feature costs (for ALL users including subscribers) const FEATURE_COSTS = { sessions: { name: 'Session-Details', cost: 500 }, 'track-editor': { name: 'Streckeneditor', cost: 1000 }, 'carcache-editor': { name: 'CarCache-Editor', cost: 1000 }, }; class AuthHandler { constructor() { this.currentUser = null; this.userSubscription = null; this.dashboardAccess = null; // { tier, expiresAt } for non-subscribers this.unlockedFeatures = {}; // { sessions: true, 'track-editor': true } this.accessLevel = 'none'; // 'permanent_vip', 'subscriber', 'credit_basic', 'credit_extended', 'credit_full', 'none' this.setupEventListeners(); this.checkAuthState(); } setupEventListeners() { document.getElementById('logout-btn')?.addEventListener('click', () => this.handleLogout()); document.getElementById('google-login-btn')?.addEventListener('click', () => this.handleGoogleLogin()); document.getElementById('apple-login-btn')?.addEventListener('click', () => this.handleAppleLogin()); } checkAuthState() { auth.onAuthStateChanged(async (user) => { if (user) { console.log('User logged in:', user.email); this.currentUser = user; const access = await this.checkAccess(user.uid); if (access.allowed) { this.accessLevel = access.level; this.showDashboard(); this.loadUserData(user.uid); this.applyAccessRestrictions(); } else { // Show access purchase options this.showAccessOptions(user.uid, access); } } else { console.log('User logged out'); this.currentUser = null; this.accessLevel = 'none'; this.showLoginScreen(); } }); } async handleGoogleLogin() { this.showError(''); try { const provider = new firebase.auth.GoogleAuthProvider(); await auth.signInWithPopup(provider); // onAuthStateChanged will handle the rest } catch (error) { if (error.code !== 'auth/popup-closed-by-user') { console.error('Google login error:', error); this.showError(`Google-Anmeldung fehlgeschlagen: ${error.message}`); } } } async handleAppleLogin() { this.showError(''); try { const provider = new firebase.auth.OAuthProvider('apple.com'); provider.addScope('email'); provider.addScope('name'); await auth.signInWithPopup(provider); // onAuthStateChanged will handle the rest } catch (error) { if (error.code !== 'auth/popup-closed-by-user') { console.error('Apple login error:', error); this.showError(`Apple-Anmeldung fehlgeschlagen: ${error.message}`); } } } async handleLogout() { try { await auth.signOut(); } catch (error) { console.error('Logout error:', error); } } /** * Check user's dashboard access level * Returns { allowed, level, subscription, credits, dashboardAccess } */ async checkAccess(uid) { try { const userEmail = this.currentUser?.email?.toLowerCase(); // 1. Permanent VIP if (userEmail && PERMANENT_VIP_EMAILS.includes(userEmail)) { console.log('Permanent VIP account:', userEmail); return { allowed: true, level: 'permanent_vip' }; } // 2. Check subscription const subSnap = await database.ref(`subscriptions/${uid}`).once('value'); const subscription = subSnap.val(); this.userSubscription = subscription; if (subscription && subscription.tier && subscription.expiresAt > Date.now()) { console.log('Subscriber access:', subscription.tier); // All subscribers get dashboard access return { allowed: true, level: 'subscriber', subscription }; } // 3. Check credit-based dashboard access const accessSnap = await database.ref(`dashboardAccess/${uid}`).once('value'); const dashAccess = accessSnap.val(); this.dashboardAccess = dashAccess; if (dashAccess && dashAccess.expiresAt > Date.now()) { console.log('Credit-based access:', dashAccess.tier); return { allowed: true, level: `credit_${dashAccess.tier}`, dashboardAccess: dashAccess }; } // 4. Check credits balance for purchase option const creditsSnap = await database.ref(`credits/${uid}/balance`).once('value'); const credits = creditsSnap.val() || 0; return { allowed: false, level: 'none', credits, subscription }; } catch (error) { console.error('Error checking access:', error); return { allowed: false, level: 'none', credits: 0 }; } } /** * Load unlocked features for user */ async loadUnlockedFeatures(uid) { try { const snap = await database.ref(`dashboardFeatures/${uid}`).once('value'); const features = snap.val() || {}; this.unlockedFeatures = {}; for (const [featureId, data] of Object.entries(features)) { if (data.expiresAt > Date.now()) { this.unlockedFeatures[featureId] = true; } } } catch (error) { console.error('Error loading features:', error); } } /** * Apply access restrictions - hide/lock tabs based on access level */ async applyAccessRestrictions() { const uid = this.currentUser?.uid; if (!uid) return; await this.loadUnlockedFeatures(uid); // Permanent VIP and subscribers with unlocked features const isPermanentVIP = this.accessLevel === 'permanent_vip'; // Determine which tabs are accessible let accessibleTabs = ['statistics', 'rankings']; // Always accessible for any logged-in user if (isPermanentVIP) { accessibleTabs = ['statistics', 'sessions', 'track-editor', 'carcache-editor', 'rankings']; } else if (this.accessLevel === 'subscriber') { // Subscribers get stats + rankings free, sessions + track-editor + carcache-editor need credits accessibleTabs = ['statistics', 'rankings']; if (this.unlockedFeatures['sessions']) accessibleTabs.push('sessions'); if (this.unlockedFeatures['track-editor']) accessibleTabs.push('track-editor'); if (this.unlockedFeatures['carcache-editor']) accessibleTabs.push('carcache-editor'); } else if (this.accessLevel.startsWith('credit_')) { const tier = this.accessLevel.replace('credit_', ''); const tierConfig = DASHBOARD_TIERS[tier]; if (tierConfig) { accessibleTabs = [...tierConfig.tabs]; } // Also check per-feature unlocks if (this.unlockedFeatures['sessions'] && !accessibleTabs.includes('sessions')) { accessibleTabs.push('sessions'); } if (this.unlockedFeatures['track-editor'] && !accessibleTabs.includes('track-editor')) { accessibleTabs.push('track-editor'); } if (this.unlockedFeatures['carcache-editor'] && !accessibleTabs.includes('carcache-editor')) { accessibleTabs.push('carcache-editor'); } } // Update nav buttons document.querySelectorAll('.nav-btn').forEach(btn => { const tab = btn.dataset.tab; const isAccessible = accessibleTabs.includes(tab); const featureCost = FEATURE_COSTS[tab]; if (!isAccessible && featureCost && !isPermanentVIP) { // Show as locked with cost btn.classList.add('locked'); btn.setAttribute('data-cost', featureCost.cost); // Add lock icon if not already there if (!btn.querySelector('.lock-icon')) { const lockSpan = document.createElement('span'); lockSpan.className = 'material-icons lock-icon'; lockSpan.textContent = 'lock'; lockSpan.style.cssText = 'font-size: 14px; margin-left: 4px; color: #ff9800;'; btn.appendChild(lockSpan); const costSpan = document.createElement('span'); costSpan.className = 'cost-label'; costSpan.textContent = `${featureCost.cost}`; costSpan.style.cssText = 'font-size: 10px; color: #ff9800; margin-left: 2px;'; btn.appendChild(costSpan); } // Override click to show purchase dialog btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); this.showFeaturePurchaseDialog(tab, featureCost); }; } else { btn.classList.remove('locked'); btn.querySelector('.lock-icon')?.remove(); btn.querySelector('.cost-label')?.remove(); btn.onclick = null; // restore default } }); // Update VIP label to show actual tier const vipLabel = document.querySelector('.vip-label'); if (vipLabel) { if (isPermanentVIP) { vipLabel.textContent = 'ADMIN'; vipLabel.style.background = 'linear-gradient(135deg, #ff0000, #ff4444)'; } else if (this.accessLevel === 'subscriber') { const tierName = this.userSubscription?.tier?.toUpperCase() || 'ABO'; vipLabel.textContent = tierName; if (this.userSubscription?.tier === 'vip') { vipLabel.style.background = 'linear-gradient(135deg, #FFD700, #FFA500)'; } else if (this.userSubscription?.tier === 'premium') { vipLabel.style.background = 'linear-gradient(135deg, #9C27B0, #E040FB)'; } else { vipLabel.style.background = 'linear-gradient(135deg, #2196F3, #64B5F6)'; } } else { const tier = this.accessLevel.replace('credit_', ''); const tierConfig = DASHBOARD_TIERS[tier]; vipLabel.textContent = tierConfig?.name?.toUpperCase() || 'GAST'; vipLabel.style.background = 'linear-gradient(135deg, #607D8B, #90A4AE)'; } } } /** * Show dialog for purchasing a locked feature */ showFeaturePurchaseDialog(featureId, featureInfo) { const existing = document.getElementById('feature-purchase-modal'); if (existing) existing.remove(); const modal = document.createElement('div'); modal.id = 'feature-purchase-modal'; modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:10000;'; modal.innerHTML = `
Schalte dieses Feature für den aktuellen Monat frei.
Web Dashboard
Angemeldet als ${this.currentUser?.email}
Dein Guthaben: ${credits.toLocaleString('de-DE')} Credits
${hasExpiredSub ? 'Dein Abonnement ist abgelaufen
' : ''}Oder abonniere in der App für dauerhaften Zugang
Web Dashboard