/** * 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 = `
lock_open

${featureInfo.name} freischalten

Schalte dieses Feature für den aktuellen Monat frei.

${featureInfo.cost.toLocaleString('de-DE')} Credits / Monat
`; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); document.getElementById('confirm-feature-purchase').addEventListener('click', async () => { await this.purchaseFeature(featureId, featureInfo.cost); modal.remove(); }); } /** * Purchase a feature with credits */ async purchaseFeature(featureId, cost) { const uid = this.currentUser?.uid; if (!uid) return; try { // Check balance const creditsSnap = await database.ref(`credits/${uid}/balance`).once('value'); const balance = creditsSnap.val() || 0; if (balance < cost) { this.showToast(`Nicht genug Credits (${balance.toLocaleString('de-DE')} / ${cost.toLocaleString('de-DE')} benötigt)`, true); return; } // Deduct credits const now = Date.now(); const d = new Date(); const nextMonth = new Date(d.getFullYear(), d.getMonth() + 1, 1); const updates = {}; updates[`credits/${uid}/balance`] = balance - cost; updates[`credits/${uid}/totalSpent`] = firebase.database.ServerValue.increment(cost); updates[`credits/${uid}/lastUpdated`] = now; updates[`credits/${uid}/transactions/${now}`] = { type: 'dashboard_feature', amount: -cost, description: `Dashboard: ${FEATURE_COSTS[featureId]?.name || featureId}`, timestamp: now, }; updates[`dashboardFeatures/${uid}/${featureId}`] = { unlockedAt: now, expiresAt: nextMonth.getTime(), creditsPaid: cost, }; await database.ref().update(updates); this.showToast(`${FEATURE_COSTS[featureId]?.name || featureId} freigeschaltet!`); this.unlockedFeatures[featureId] = true; this.applyAccessRestrictions(); // Switch to the unlocked tab if (window.dashboard) { window.dashboard.switchTab(featureId); } } catch (error) { console.error('Error purchasing feature:', error); this.showToast('Fehler beim Freischalten', true); } } /** * Show access tier purchase options for non-subscribers */ showAccessOptions(uid, accessInfo) { const loginScreen = document.getElementById('login-screen'); const loginContainer = loginScreen.querySelector('.login-container'); // Check if user has any subscription at all const hasExpiredSub = accessInfo.subscription && accessInfo.subscription.expiresAt <= Date.now(); const credits = accessInfo.credits || 0; loginContainer.innerHTML = `

Angemeldet als ${this.currentUser?.email}

Dein Guthaben: ${credits.toLocaleString('de-DE')} Credits

${hasExpiredSub ? '

Dein Abonnement ist abgelaufen

' : ''}

Dashboard-Zugang per Credits

Oder abonniere in der App für dauerhaften Zugang

${this._renderAccessTier('basic', credits)} ${this._renderAccessTier('extended', credits)} ${this._renderAccessTier('full', credits)}
`; } _renderAccessTier(tierKey, userCredits) { const tier = DASHBOARD_TIERS[tierKey]; const canAfford = userCredits >= tier.cost; const tabLabels = { 'statistics': 'Statistiken', 'rankings': 'Ranglisten', 'sessions': 'Sessions', 'track-editor': 'Streckeneditor', }; const included = tier.tabs.map(t => tabLabels[t] || t).join(', '); return `
${tier.name} ${tier.cost.toLocaleString('de-DE')} Credits/Monat
${included}
`; } /** * Purchase dashboard access tier with credits */ async purchaseDashboardAccess(tierKey) { const uid = this.currentUser?.uid; if (!uid) return; const tier = DASHBOARD_TIERS[tierKey]; if (!tier) return; try { const creditsSnap = await database.ref(`credits/${uid}/balance`).once('value'); const balance = creditsSnap.val() || 0; if (balance < tier.cost) { this.showToast('Nicht genug Credits', true); return; } const now = Date.now(); const d = new Date(); const nextMonth = new Date(d.getFullYear(), d.getMonth() + 1, 1); const updates = {}; updates[`credits/${uid}/balance`] = balance - tier.cost; updates[`credits/${uid}/totalSpent`] = firebase.database.ServerValue.increment(tier.cost); updates[`credits/${uid}/lastUpdated`] = now; updates[`credits/${uid}/transactions/${now}`] = { type: 'dashboard_access', amount: -tier.cost, description: `Dashboard ${tier.name}-Zugang (1 Monat)`, timestamp: now, }; updates[`dashboardAccess/${uid}`] = { tier: tierKey, activatedAt: now, expiresAt: nextMonth.getTime(), creditsPaid: tier.cost, }; await database.ref().update(updates); this.dashboardAccess = { tier: tierKey, expiresAt: nextMonth.getTime() }; this.accessLevel = `credit_${tierKey}`; this.showToast(`Dashboard ${tier.name}-Zugang freigeschaltet!`); this.showDashboard(); this.loadUserData(uid); this.applyAccessRestrictions(); } catch (error) { console.error('Error purchasing dashboard access:', error); this.showToast('Fehler beim Freischalten', true); } } async loadUserData(uid) { try { const userRef = database.ref(`users/${uid}`); const userSnapshot = await userRef.once('value'); const userData = userSnapshot.val(); if (userData?.nickname) { document.getElementById('user-nickname').textContent = userData.nickname; } else { const carRef = database.ref(`cars/${uid}`); const carSnapshot = await carRef.once('value'); const carData = carSnapshot.val(); if (carData?.nickname) { document.getElementById('user-nickname').textContent = carData.nickname; } } } catch (error) { console.error('Error loading user data:', error); } } showError(message) { const errorEl = document.getElementById('login-error'); if (errorEl) errorEl.textContent = message; } showToast(message, isError = false) { const toast = document.createElement('div'); toast.style.cssText = `position:fixed;top:20px;right:20px;padding:12px 24px;border-radius:8px;color:#fff;font-weight:500;z-index:99999;transition:opacity 0.3s;${isError ? 'background:#f44336;' : 'background:#4CAF50;'}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 3000); } showLoginScreen() { document.getElementById('login-screen').classList.remove('hidden'); document.getElementById('dashboard').classList.add('hidden'); // Restore original login form if it was replaced by access options const loginContainer = document.querySelector('#login-screen .login-container'); if (loginContainer && !loginContainer.querySelector('#google-login-btn')) { loginContainer.innerHTML = `
star Dashboard Feature

Melde dich mit deinem Account an

`; // Re-attach event listeners document.getElementById('google-login-btn')?.addEventListener('click', () => this.handleGoogleLogin()); document.getElementById('apple-login-btn')?.addEventListener('click', () => this.handleAppleLogin()); } } showDashboard() { document.getElementById('login-screen').classList.add('hidden'); document.getElementById('dashboard').classList.remove('hidden'); if (window.dashboard) { window.dashboard.initialize(); } } getCurrentUser() { return this.currentUser; } /** * Check if a specific tab is accessible for current user */ canAccessTab(tabName) { if (this.accessLevel === 'permanent_vip') return true; if (['statistics', 'rankings'].includes(tabName)) return true; if (this.accessLevel === 'subscriber') { return this.unlockedFeatures[tabName] === true; } if (this.accessLevel.startsWith('credit_')) { const tier = this.accessLevel.replace('credit_', ''); const tierConfig = DASHBOARD_TIERS[tier]; if (tierConfig?.tabs.includes(tabName)) return true; return this.unlockedFeatures[tabName] === true; } return false; } } // Initialize auth handler const authHandler = new AuthHandler(); window.authHandler = authHandler;