1<!DOCTYPE html> 2<html lang="ro"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <meta name="base-path" content=""> 7 <title>Cardora - Accesul tau la carduri digitale de fidelizare pentru afaceri moderne</title> 8 <meta name="description" content="Cardora ajuta afacerile moderne sa fidelizeze clientii cu carduri digitale simple, rapide si usor de folosit."> 9 10 <link rel="canonical" href="https://cardora.ro/" class="yoast-seo-meta-tag"> 11 <meta property="og:locale" content="ro_RO" class="yoast-seo-meta-tag"> 12 <meta property="og:type" content="website" class="yoast-seo-meta-tag"> 13 <meta property="og:title" content="Cardora - Accesul tau la carduri digitale de fidelizare pentru afaceri moderne" class="yoast-seo-meta-tag"> 14 <meta property="og:description" content="Cardora ajuta afacerile moderne sa fidelizeze clientii cu carduri digitale simple, rapide si usor de folosit." class="yoast-seo-meta-tag"> 15 <meta property="og:url" content="https://cardora.ro/" class="yoast-seo-meta-tag"> 16 <meta property="og:site_name" content="Cardora - Solutia digitala de fidelizare pentru afacerea ta" class="yoast-seo-meta-tag"> 17 <meta property="og:image" content="https://cardora.ro/wp-content/uploads/2025/04/cardora-icon-white-black.png" class="yoast-seo-meta-tag"> 18 <meta property="og:image:width" content="314" class="yoast-seo-meta-tag"> 19 <meta property="og:image:height" content="324" class="yoast-seo-meta-tag"> 20 <meta property="og:image:type" content="image/png" class="yoast-seo-meta-tag"> 21 <meta property="og:image:alt" content="Logo Cardora" class="yoast-seo-meta-tag"> 22 <meta name="twitter:card" content="summary" class="yoast-seo-meta-tag"> 23 <meta name="twitter:title" content="Cardora - Accesul tau la carduri digitale de fidelizare pentru afaceri moderne" class="yoast-seo-meta-tag"> 24 <meta name="twitter:description" content="Cardora ajuta afacerile moderne sa fidelizeze clientii cu carduri digitale simple, rapide si usor de folosit." class="yoast-seo-meta-tag"> 25 <meta name="twitter:image" content="https://cardora.ro/wp-content/uploads/2025/04/cardora-icon-white-black.png" class="yoast-seo-meta-tag"> 26 <meta name="twitter:image:alt" content="Logo Cardora" class="yoast-seo-meta-tag"> 27 28 29 <!-- Common styles --> 30 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> 31 <link rel="stylesheet" href="/assets/css/style.css"> 32 <link rel="stylesheet" href="/assets/css/neocards.css?t=1777123850"> 33 34 <!-- Firebase UI CSS --> 35 <link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/6.0.1/firebase-ui-auth.css" /> 36 37 <!-- Favicons --> 38 <link rel="apple-touch-icon" sizes="180x180" href="/assets/ico/apple-touch-icon.png"> 39 <link rel="icon" type="image/png" sizes="32x32" href="/assets/ico/favicon-32x32.png"> 40 <link rel="icon" type="image/png" sizes="16x16" href="/assets/ico/favicon-16x16.png"> 41 <link rel="manifest" href="/assets/ico/site.webmanifest"> 42 43 <!-- Fonts --> 44 <link rel="preconnect" href="https://fonts.googleapis.com"> 45 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 46 <link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"> 47</head> 48<body> 49 <!-- Language Switcher --> 50 <div class="lang-switcher" id="langSwitcher"> 51 <button class="lang-switcher-btn" id="langSwitcherBtn" type="button" aria-label="Change language"> 52 <i class="fas fa-globe"></i> 53 <span>RO</span> 54 </button> 55 <div class="lang-switcher-menu" id="langSwitcherMenu"> 56 <button class="lang-switcher-option active" 57 data-locale="ro" type="button"> 58 RomânÄ </button> 59 <button class="lang-switcher-option" 60 data-locale="en" type="button"> 61 English </button> 62 <button class="lang-switcher-option" 63 data-locale="de" type="button"> 64 Deutsch </button> 65 <button class="lang-switcher-option" 66 data-locale="zh" type="button"> 67 䏿 </button> 68 <button class="lang-switcher-option" 69 data-locale="hu" type="button"> 70 Magyar </button> 71 </div> 72 </div> 73 <style> 74 .lang-switcher{position:fixed;top:16px;right:16px;z-index:1050;font-family:inherit} 75 .lang-switcher-btn{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);border:1px solid rgba(0,0,0,.1);border-radius:8px;padding:6px 12px;cursor:pointer;font-size:13px;color:#333;box-shadow:0 2px 8px rgba(0,0,0,.08);transition:background .15s} 76 .lang-switcher-btn:hover{background:rgba(255,255,255,1)} 77 .lang-switcher-btn i{font-size:14px;color:#555} 78 .lang-switcher-menu{display:none;position:absolute;top:calc(100% + 4px);right:0;background:#fff;border:1px solid rgba(0,0,0,.1);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12);overflow:hidden;min-width:120px} 79 .lang-switcher-menu.open{display:block}
80 .lang-switcher-option{display:block;width:100%;text-align:left;padding:8px 14px;border:none;background:none;cursor:pointer;font-size:13px;color:#333;transition:background .1s} 81 .lang-switcher-option:hover{background:#f0f0f0} 82 .lang-switcher-option.active{font-weight:600;color:#2E5A4E} 83 </style> 84
84<script> 85 (function(){ 86 var btn=document.getElementById('langSwitcherBtn'); 87 var menu=document.getElementById('langSwitcherMenu'); 88 btn.addEventListener('click',function(e){e.stopPropagation();menu.classList.toggle('open')}); 89 document.addEventListener('click',function(){menu.classList.remove('open')}); 90 menu.addEventListener('click',function(e){ 91 var opt=e.target.closest('.lang-switcher-option'); 92 if(!opt||opt.classList.contains('active'))return; 93 var locale=opt.dataset.locale; 94 var basePath=document.querySelector('meta[name="base-path"]')?.content||''; 95 fetch(basePath+'/api/v1/set-locale',{ 96 method:'POST', 97 headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}, 98 credentials:'same-origin', 99 body:JSON.stringify({language:locale}) 100 }).then(function(){location.reload()}).catch(function(){location.reload()}); 101 }); 102 })(); 103 </script>
103 104 105 <!-- Main content --> 106 107 <!-- Firebase Scripts --> 108
108<script src="https://www.gstatic.com/firebasejs/11.3.0/firebase-app-compat.js"></script>
108 109
109<script src="https://www.gstatic.com/firebasejs/11.3.0/firebase-auth-compat.js"></script>
109 110
110<script src="https://www.gstatic.com/firebasejs/ui/6.0.1/firebase-ui-auth__ro.js"></script>
110 111 112
112<script> 113 const firebaseConfig = { 114 apiKey: "AIzaSyBCSldLSWylAo4Oxve7bMf12USfkvWHBnY", 115 authDomain: "app.cardora.ro", 116 projectId: "neocards-57c99", 117 storageBucket: "neocards-57c99.firebasestorage.app", 118 messagingSenderId: "1071888118364", 119 appId: "1:1071888118364:web:9859f4590961b363562c99", 120 measurementId: "G-MGTQD45C3M" 121 }; 122 if (!firebase.apps.length) { 123 firebase.initializeApp(firebaseConfig); 124 } 125 else { 126 } 127 </script>
127 128 129 <!-- Include directly the auth.js file --> 130
130<script> 131 // Get base path from meta tag or environment 132// TODO: this should be computed in another script, not here in the auth.js file 133const basePath = document.querySelector('meta[name="base-path"]')?.content || ''; 134 135// Initialize the FirebaseUI Widget using Firebase. 136// handle this with card, maybe firebase does not exist at this point 137var ui = null; 138try 139{ 140 ui = new firebaseui.auth.AuthUI(firebase.auth()); 141} 142catch(error) 143{ 144 console.error('Error initializing FirebaseUI:', error); 145} 146 147/** 148 * Clear stale Firebase/FirebaseUI auth state from sessionStorage. 149 * This removes entries that cause the "missing initial state" error 150 * when sessionStorage gets out of sync (e.g. storage partitioning, 151 * mobile browsers clearing storage, popup blockers falling back to 152 * redirect flow, etc.). 153 */ 154function clearStaleAuthState() { 155 try { 156 const keysToRemove = []; 157 for (let i = 0; i < sessionStorage.length; i++) { 158 const key = sessionStorage.key(i); 159 if (key && (key.startsWith('firebase:') || key.startsWith('firebaseui:'))) { 160 keysToRemove.push(key); 161 } 162 } 163 keysToRemove.forEach(function(key) { sessionStorage.removeItem(key); }); 164 if (keysToRemove.length > 0) { 165 console.log('[auth] Cleared ' + keysToRemove.length + ' stale Firebase sessionStorage entr
165ies'); 166 } 167 } catch (e) { 168 // sessionStorage may be completely inaccessible (private browsing, etc.) 169 console.warn('[auth] Could not access sessionStorage:', e.message); 170 } 171} 172 173/** 174 * Handle any pending redirect results from a previous sign-in attempt. 175 * On mobile browsers, popups can be blocked and FirebaseUI may silently 176 * fall back to redirect flow. When the page reloads after the redirect, 177 * we need to process the result here. 178 */ 179function handlePendingRedirectResult() { 180 if (!firebase || !firebase.auth) return; 181 182 firebase.auth().getRedirectResult() 183 .then(function(result) { 184 if (result && result.user) { 185 console.log('[auth] Redirect sign-in completed for:', result.user.email); 186 // Process exactly like signInSuccessWithAuthResult 187 handleAuthResult({ user: result.user }); 188 } 189 }) 190 .catch(function(error) { 191 // The "missing initial state" error surfaces here when using redirect flow 192 console.warn('[auth] getRedirectResult error:', error.code, error.message); 193 194 if (error.message && error.message.indexOf('missing initial state') !== -1) { 195 console.log('[auth] Detected missing-initial-state error, clearing stale auth state and re-initialising UI'); 196 clearStaleAuthState(); 197 // Re-initialize the UI so the user can try again cleanly 198 setTimeout(function() { initializeAuthUI(); }, 300); 199 } 200 }); 201} 202 203/** 204 * Shared handler that sends the Firebase user to the backend and 205 * performs the appropriate redirect. Used by both the popup callback 206 * and the redirect-result handler. 207 */ 208function handleAuthResult(authResult) { 209 authResult.user.getIdToken() 210 .then(function(idToken) { 211 // Default to true â mobile browsers aggressively expire sessions 212 const rememberCheckbox = document.getElementById('remember-me'); 213 const rememberMe = rememberCheckbox ? rememberCheckbox.checked : true; 214 215 // On the card join page, send the join context along with the token. 216 // The server prefers its session copy; this is the recovery path for 217 // when the session cookie got replaced mid-auth (e.g. Apple's 218 // cross-site form_post response to the auth popup). 219 var loginPayload = { 220 idToken: idToken, 221 rememberMe: rememberMe 222 }; 223 if (window.joinSessionId && window.joinCardId) { 224 loginPayload.join_redirect = window.location.pathname + window.location.search; 225 loginPayload.join_session = window.joinSessionId; 226 } 227 228 // Send ID token to backend 229 return fetch(basePath + '/api/v1/auth/login', { 230 method: 'POST', 231 headers: { 232 'Content-Type': 'application/json', 233 'X-Requested-With': 'XMLHttpRequest' 234 }, 235 body: JSON.stringify(loginPayload) 236 }); 237 }) 238 .then(function(response) { 239 if (!response.ok) { 240 return response.json().then(function(err) { return Promise.reject(err); }); 241 } 242 return response.json(); 243 }) 244 .then(function(response) { 245 if (response.success && response.data) { 246 var data = response.data; 247 248 // Store CSRF token 249 if (data.csrf_token) { 250 localStorage.setItem('csrf_token', data.csrf_token); 251 } 252 253 // If we have a direct redirect path, use it 254 if (data.redirect) { 255 var redirectPath = data.redirect.startsWith(basePath) ? data.redirect : basePath + data.redirect; 256 window.location.replace(redirectPath); 257 return; 258 } 259 260 // Otherwise, handle role-based redirect 261 if (data.user && data.user.roles) { 262 var rolePath; 263 if (data.user.roles.length > 1) { 264 rolePath = '/role-select'; 265 } else if (data.user.roles.length === 1) { 266 switch (data.user.roles[0]) { 267 case 'admin': 268 rolePath = '/admin/dashboard'; 269 break; 270 case 'clerk': 271 rolePath = '/clerk/dashboard'; 272 break; 273 case 'customer': 274 rolePath = '/customer/dashboard'; 275 break; 276 } 277 } 278 279 if (rolePath) { 280 console.log('Redirecting to:', basePath + rolePath); 281 window.location.replace(basePath + rolePath); 282 } 283 } 284 } else { 285 console.error('Login failed:', response.error || 'Unknown error'); 286 showError('Login failed: ' + (response.error || 'Unknown error')); 287 } 288 }) 289 .catch(function(error) { 290 console.error('Auth error:', error); 291 showError('Authentication failed: ' + (error.message || 'Unknown error')); 292 }); 293} 294 295// Configuration for FirebaseUI 296function getUiConfig() { 297 // Construct absolute URLs for ToS and Privacy Policy 298 // Use window.basePath if available (set in layout.php), otherwise fall back to basePath from meta tag 299 const currentBasePath = window.basePath || basePath || ''; 300 const tosUrl = window.location.origin + currentBasePath + '/tos.html'; 301 const privacyPolicyUrl = window.location.origin + currentBasePath + '/privacy.html'; 302 303 return { 304 callbacks: { 305 signInFailure: function(error) { 306 // Handle the "missing initial state" error specifically 307 // This happens when sessionStorage is inaccessible or was cleared 308 // (storage partitioning in mobile browsers, ITP in Safari, etc.) 309 if (error.message && error.message.indexOf('missing initial state') !== -1) { 310 console.warn('[auth] signInFailure: missing initial state detected'); 311 clearStaleAuthState(); 312 showRetryableError(); 313 return Promise.resolve(); 314 } 315 316 // Handle specific Firebase Auth errors 317 if (error.code === 'firebaseui/anonymous-upgrade-merge-conflict') { 318 return firebaseui.auth.AnonymousUpgradeConflictResolver.defaultCallback(error); 319 } 320 if (error.code === 'auth/account-exists-with-different-credential') { 321 // Let Firebase UI handle account linking 322 return Promise.resolve(); 323 } 324 if (error.code === 'auth/email-already-in-use') { 325 console.log('Email already in use, user should sign in instead'); 326 return Promise.resolve(); 327 } 328 // Join funnel: log client-side auth failure when on card join page 329 if (window.joinSessionId && window.joinCardId) { 330 sendJoinFunnelBeacon('auth_failed', { 331 error_code: error.code || error.message || 'unknown' 332 }); 333 } 334 console.error('Sign in error:', error); 335 return Promise.resolve(); 336 }, 337 signInSuccessWithAuthResult: function(authResult, redirectUrl) { 338 // Mark auth as completed so page_abandoned beacon doesn't fire on redirect 339 _funnelAuthCompleted = true; 340 _funnelStage = 'auth_completed'; 341 if (_funnelCancelTimer) { 342 clearTimeout(_funnelCancelTimer); 343 _funnelCancelTimer = null; 344 } 345 // Handle the auth flow but always return false to prevent FirebaseUI redirect 346 handleAuthResult(authResult); 347 348 // Always return false to prevent FirebaseUI's default redirect 349 return false;
350 }, 351 uiShown: function() { 352 // The widget is rendered. 353 const loader = document.getElementById('loader'); 354 if (loader) { 355 loader.style.display = 'none'; 356 } 357 // Join funnel: log that Firebase UI actually rendered 358 if (_funnelStage === 'loading') { 359 _funnelStage = 'ui_ready'; 360 } 361 sendJoinFunnelBeacon('auth_ui_ready'); 362 // Attach click listeners to auth provider buttons for method selection tracking 363 attachAuthMethodListeners(); 364 } 365 }, 366 signInOptions: [ 367 // Enable Google sign-in 368 // Enable Facebook sign-in 369 { 370 provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID, 371 buttonColor: '#FF0000', 372 customParameters: { 373 locale: 'ro' 374 } 375 }, 376 // TODO: Facebook login temporarily disabled - uncomment when ready to re-enable 377 // { 378 // provider: firebase.auth.FacebookAuthProvider.PROVIDER_ID, 379 // }, 380 // Enable email/password with proper configuration 381 { 382 provider: firebase.auth.EmailAuthProvider.PROVIDER_ID, 383 buttonColor: '#2E5A4E', 384 customParameters: { 385 locale: 'ro' 386 } 387 }, 388 // Enable Apple sign-in with scopes and locale 389 { 390 provider: 'apple.com', 391 scopes: ['email', 'name'], 392 buttonColor: '#000000', 393 customParameters: { 394 locale: 'en' 395 } 396 } 397 ], 398 // Use popup for IDP Providers sign-in flow instead of the default, redirect. 399 // This avoids the "missing initial state" error that occurs with redirect flow 400 // in storage-partitioned browsers (Safari ITP, Chrome 3P cookie deprecation). 401 signInFlow: 'popup', 402 // NONE disables the accountchooser.com redirect which was a major cause of the 403 // "missing initial state" error. ACCOUNT_CHOOSER_COM is deprecated by Google 404 // and caused a cross-origin redirect that lost sessionStorage on many mobile browsers. 405 credentialHelper: firebaseui.auth.CredentialHelper.NONE, 406 // Terms of Service and Privacy Policy URLs 407 // FirebaseUI will display a message that by clicking "Sign in", users accept these terms 408 tosUrl: tosUrl, 409 privacyPolicyUrl: privacyPolicyUrl 410 }; 411} 412 413// ââ Join funnel beacons âââââââââââââââââââââââââââââââââââââââââââââââââââââ 414// Lightweight fire-and-forget events sent to funnel.log via the beacon endpoint. 415// Only active on the card join page (window.joinSessionId is set by join.php). 416 417var _funnelAuthCompleted = false; 418var _funnelSentEvents = {}; 419// Funnel stage the page is currently at; sent with page_abandoned/auth_cancelled 420// so drop-off can be attributed to a step (the OAuth popup hides the page, which 421// without the stage looks identical to a real abandonment). 422var _funnelStage = 'loading'; 423var _funnelCancelTimer = null; 424 425function sendJoinFunnelBeacon(event, extra) { 426 if (!window.joinSessionId || !window.joinCardId) return; 427 if (_funnelSentEvents[event]) return; 428 _funnelSentEvents[event] = true; 429 430 var bp = window.basePath || (typeof basePath !== 'undefined' ? basePath : ''); 431 var url = bp + '/api/v1/cards/' + window.joinCardId + '/join-funnel-event'; 432 var payload = Object.assign({ 433 event: event, 434 context: window.joinSessionId, 435 auth_required: window.joinAuthRequired !== false, 436 user_state: window.joinUserState || (window.joinAuthRequired === false ? 'authenticated' : 'anonymous') 437 }, extra || {}); 438 try { 439 fetch(url, { 440 method: 'POST', 441 headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, 442 body: JSON.stringify(payload), 443 keepalive: true 444 }).catch(function() {}); 445 } catch(e) {} 446} 447 448function attachAuthMethodListeners() { 449 if (!window.joinSessionId) return; 450 // FirebaseUI renders .firebaseui-idp-button for each provider 451 var buttons = document.querySelectorAll('.firebaseui-idp-button');
452 buttons.forEach(function(btn) { 453 btn.addEventListener('click', function() { 454 var providerId = btn.getAttribute('data-provider-id') || 'unknown'; 455 _funnelStage = 'method_selected'; 456 sendJoinFunnelBeacon('auth_method_selected', { method: providerId }); 457 }, { once: true }); 458 }); 459} 460 461// Track page abandonment: fires when user leaves the join page without completing auth. 462// The stage disambiguates a real bounce (ui_ready) from the OAuth popup hiding the 463// page mid-flow (method_selected). 464if (typeof window !== 'undefined' && window.joinSessionId) { 465 document.addEventListener('visibilitychange', function() { 466 if (document.visibilityState === 'hidden' && !_funnelAuthCompleted) { 467 sendJoinFunnelBeacon('page_abandoned', { stage: _funnelStage }); 468 } 469 if (document.visibilityState === 'visible') { 470 armAuthCancelledDetector(); 471 } 472 }); 473 window.addEventListener('focus', armAuthCancelledDetector); 474} 475 476// The user picked a provider, the OAuth popup opened, and now the page is back in 477// the foreground without a completed sign-in: if auth still hasn't completed after 478// a grace period, they closed/cancelled the popup. FirebaseUI is silent in that 479// case (signInFailure never fires for popup-closed-by-user), which previously made 480// cancelled auth indistinguishable from lost sessions. 481function armAuthCancelledDetector() { 482 if (_funnelStage !== 'method_selected' || _funnelAuthCompleted) return; 483 if (_funnelSentEvents['auth_cancelled'] || _funnelCancelTimer) return; 484 _funnelCancelTimer = setTimeout(function() { 485 _funnelCancelTimer = null; 486 if (!_funnelAuthCompleted && _funnelStage === 'method_selected') { 487 sendJoinFunnelBeacon('auth_cancelled', { stage: _funnelStage }); 488 } 489 }, 12000); 490} 491 492// Initialize FirebaseUI 493function initializeAuthUI() { 494 const container = document.getElementById('firebaseui-auth-container'); 495 if (container) { 496 // Clear any existing content 497 container.innerHTML = ''; 498 // If the FirebaseUI instance has a pending redirect, reset it first 499 if (ui) { 500 try { 501 ui.reset(); 502 } catch (e) { 503 // Ignore reset errors 504 } 505 // Start the UI 506 ui.start('#firebaseui-auth-container', getUiConfig()); 507 } 508 } 509} 510 511// Show error message 512function showError(message) { 513 const errorDiv = document.createElement('div'); 514 errorDiv.className = 'error'; 515 errorDiv.textContent = message; 516 const container = document.getElementById('firebaseui-auth-container'); 517 container.innerHTML = ''; 518 container.appendChild(errorDiv); 519 // Reinitialize UI after error 520 setTimeout(() => initializeAuthUI(), 3000); 521} 522 523/** 524 * Show a user-friendly error for the "missing initial state" problem 525 * with a one-tap retry button so the user doesn't have to restart the browser. 526 */ 527function showRetryableError() { 528 const container = document.getElementById('firebaseui-auth-container'); 529 if (!container) return; 530 531 container.innerHTML = ''; 532 533 const wrapper = document.createElement('div'); 534 wrapper.style.cssText = 'text-align:center; padding:1.5rem 1rem;'; 535 536 const msg = document.createElement('p'); 537 msg.style.cssText = 'color:#666; margin-bottom:1rem; font-size:0.95rem; line-height:1.4;'; 538 msg.textContent = 'A apÄrut o problemÄ cu autentificarea. Te rugÄm sÄ Ã®ncerci din nou.'; 539 540 const btn = document.createElement('button'); 541 btn.type = 'button'; 542 btn.textContent = 'ReîncearcÄ'; 543 btn.style.cssText = 'background:#2E5A4E; color:#fff; border:none; padding:0.65rem 2rem; border-radius:6px; font-size:1rem; cursor:pointer;'; 544 btn.addEventListener('click', function() { 545 clearStaleAuthState(); 546 initializeAuthUI(); 547 }); 548 549 wrapper.appendChild(msg); 550 wrapper.appendChild(btn); 551 container.appendChild(wrapper); 552} 553 554// Initialize when DOM is ready 555document.addEventListener('DOMContentLoaded', function() { 556 // First, try to process any pending redirect result (in case popup was blocked 557 // on a previous attempt and the browser fell back to redirect flow). 558 handlePendingRedirectResult(); 559 560 // Then render the sign-in UI 561 initializeAuthUI(); 562}); 563 564// Handle logout 565function signOut() { 566 // First sign out from Firebase 567 firebase.auth().signOut().then(function() { 568 // Clear all local storage 569 localStorage.clear(); 570 571 // Create and submit a form to handle the logout 572 const form = document.createElement('form'); 573 form.method = 'GET'; 574 form.action = basePath + '/logout'; 575 576 // Add any necessary hidden fields 577 const csrfInput = document.createElement('input'); 578 csrfInput.type = 'hidden'; 579 csrfInput.name = 'csrf_token'; 580 csrfInput.value = localStorage.getItem('csrf_token'); 581 form.appendChild(csrfInput); 582 583 // Add the form to the document and submit it 584 document.body.appendChild(form); 585 form.submit(); 586 }).catch(function(error) { 587 console.error('Sign out error:', error); 588 // If Firebase signout fails, still try to logout from our backend 589 window.location.href = basePath + '/logout'; 590 }); 591} 592 593// Make signOut function globally available 594window.signOut = signOut; 595 596// Function to handle successful authentication 597async function handleAuthSuccess(user) { 598 try { 599 // Get the ID token 600 const idToken = await user.getIdToken(); 601 602 // Send the token to backend 603 const response = await fetch(basePath + '/api/v1/auth/login', { 604 method: 'POST', 605 headers: { 606 'Content-Type': 'application/json', 607 }, 608 body: JSON.stringify({ idToken }) 609 }); 610 611 if (!response.ok) { 612 throw new Error('Failed to authenticate with backend'); 613 } 614 615 const data = await response.json(); 616 617 // Store any necessary data in localStorage 618 localStorage.setItem('user_email', user.email); 619 localStorage.setItem('display_name', user.displayName || ''); 620 621 // Redirect based on user roles 622 if (data.roles && data.roles.length > 0) { 623 if (data.roles.length === 1) { 624 // Single role - redirect directly to appropriate dashboard 625 switch (data.roles[0]) { 626 case 'admin': 627 window.location.href = basePath + '/admin/dashboard'; 628 break; 629 case 'clerk': 630 window.location.href = basePath + '/clerk/dashboard'; 631 break; 632 case 'customer': 633 window.location.href = basePath + '/customer/cards'; 634 break; 635 } 636 } else { 637 // Multiple roles - redirect to role selection page 638 window.location.href = basePath + '/role-select'; 639 } 640 } else { 641 // No roles assigned - redirect to customer registration 642 window.location.href = basePath + '/customer/register'; 643 } 644 } catch (error) { 645 console.error('Error during authentication:', error); 646 showError('Authentication failed. Please try again.'); 647 } 648} </script>
648 649 650 <!-- Bootstrap --> 651 <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> 652
652<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
652 653</body> 654</html> 655 656 657 658 659<!-- Mobile-only CSS background (waves/blobs + logo). Video is not loaded on mobile. --> 660<div class="login-mobile-bg" aria-hidden="true"> 661 <div class="login-mobile-blobs"> 662 <div class="login-blob login-blob-1"></div> 663 <div class="login-blob login-blob-2"></div> 664 <div class="login-blob login-blob-3"></div> 665 <div class="login-blob login-blob-4"></div> 666 <div class="login-blob login-blob-5"></div> 667 </div> 668 <div class="login-mobile-logo-wrap"> 669 <img src="/assets/images/logo.png" alt="" class="login-mobile-logo" width="120" height="120"> 670 </div> 671</div> 672 673<div class="container login-page-container"> 674 <div class="row"> 675 <div class="col col-sm-12 col-md-6 col-12 login-video-col"> 676 <div class="video-col"> 677 <video autoplay muted loop playsinline class="bg-video" style="z-index: -1;"> 678 <source data-src="/assets/videos/capybara_green.mp4" type="video/mp4"> 679 </video> 680 </div> 681 </div> 682 <div class="col col-sm-12 col-md-6 col-12"> 683 <div class="login-page"> 684 <div class="login-container"> 685 <a href="https://cardora.ro"> 686 <img style="width: 200px; height: 48px; margin-bottom: 4rem;" src="https://cardora.ro/wp-content/uploads/2025/04/cardora-black-logo-1-200x48.png" alt="Cardora" class="cardora-icon"> <br/> 687 </a> 688 <h1>Bine ai revenit</h1> 689 <h3 style="margin-bottom: 3rem;">Alege una dintre opÈiuni</h3> 690 691 692 <!-- The loader --> 693 <div id="loader" style="display: none;">Loading...</div> 694 <div id="firebaseui-skeleton"> 695 <div class="skeleton-line" style="width: 80%; height: 40px; margin: 0 auto 16px;"></div> 696 <div class="skeleton-line" style="width: 60%; height: 40px; margin: 0 auto 16px;"></div> 697 <div class="skeleton-line" style="width: 90%; height: 40px; margin: 0 auto 16px;"></div> 698 </div> 699 <div id="firebaseui-auth-container" style="display: none;"></div> 700 701 <!-- Remember me checkbox --> 702 <div class="remember-me-container" style="margin: 1rem; text-align: center;"> 703 <label class="remember-me-label" style="display: flex; align-items: center; cursor: pointer; font-size: 0.9rem; color: #666;"> 704 <input type="checkbox" id="remember-me" style="margin-right: 0.5rem;" checked> 705 Èine minte acest dispozitiv </label> 706 </div> 707 <span>Nu ai încÄ un cont?</span> <a href="https://app.cardora.ro">ÃnregistreazÄ afacerea ta</a> 708 </div> 709 </div> 710 </div> 711 </div> 712</div> 713 714 715 716<!-- Note: Firebase compat SDK and FirebaseUI are loaded by auth_layout.php. 717 No duplicate initialization needed here. --> 718 719<!-- Generate CSRF token -->
720<script> 721 // Store CSRF token for use in requests 722 window.csrfToken = "839af1808b7f772936b674c7d1f858aa783d732ce7b15db11cbc4c1549edfce1"; 723</script>
723 724 725
726<script> 727 // Wait for FirebaseUI to render, then hide the skeleton 728 function hideSkeletonWhenReady() { 729 const observer = new MutationObserver(() => { 730 if (document.querySelector('#firebaseui-auth-container .firebaseui-container')) { 731 document.getElementById('firebaseui-skeleton').style.display = 'none'; 732 document.getElementById('firebaseui-auth-container').style.display = 'block'; 733 observer.disconnect(); 734 } 735 }); 736 observer.observe(document.getElementById('firebaseui-auth-container'), { childList: true, subtree: true }); 737 } 738 hideSkeletonWhenReady(); 739</script>
739 740 741 742 743 744<style> 745.login-page { 746 min-height: 100vh; 747 display: flex; 748 align-items: center; 749 justify-content: center; 750 background: #f5f5f5; 751} 752 753.login-container { 754 background: white; 755 padding: 2rem; 756 border-radius: 8px; 757 box-shadow: 0 2px 4px rgba(0,0,0,0.1); 758 width: 100%; 759 max-width: 400px; 760 text-align: center; 761} 762 763#loader { 764 text-align: center; 765 padding: 20px; 766} 767 768.error { 769 color: red; 770 padding: 1rem; 771 border: 1px solid red; 772 border-radius: 4px; 773 margin: 1rem 0; 774} 775/* .firebaseui-idp-text.firebaseui-idp-text-long {display: none !important;} 776.firebaseui-idp-google { 777 background-color: #4285F4 !important; 778 color: white !important; 779 780} 781.firebaseui-idp-google .firebaseui-idp-text { 782 color: white !important; 783} */ 784 785 .login-page { 786 /* Keyword value */ 787 background-color: rgb(255 255 255 / 30%); 788 backdrop-filter: blur(10px); 789 } 790 791#firebaseui-skeleton { 792 display: block; 793 margin-bottom: 2rem; 794} 795.skeleton-line { 796 background: linear-gradient(90deg, #eee 25%, #ddd 37%, #eee 63%); 797 background-size: 400% 100%; 798 animation: skeleton-loading 1.2s infinite linear; 799 border-radius: 6px; 800} 801@keyframes skeleton-loading { 802 0% { 803 background-position: 100% 50%; 804 } 805 100% { 806 background-position: 0 50%; 807 } 808} 809 810/* --- Mobile-only background (no video load) --- */ 811.login-mobile-bg { 812 display: none; 813} 814.login-video-col { 815 display: block; 816} 817@media (max-width: 767px) { 818 .login-mobile-bg { 819 display: block; 820 position: fixed; 821 inset: 0; 822 z-index: 0; 823 overflow: hidden; 824 background: linear-gradient(160deg, #e8f4f2 0%, #d4eae6 30%, #b8ddd6 60%, #9dd1c9 100%); 825 } 826 .login-mobile-blobs { 827 position: absolute; 828 inset: -30%; 829 pointer-events: none; 830 } 831 .login-blob { 832 position: absolute; 833 border-radius: 50%; 834 filter: blur(60px); 835 opacity: 0.55; 836 will-change: transform; 837 } 838 .login-blob-1 { 839 width: 70vmin; 840 height: 70vmin; 841 background: radial-gradient(circle, rgba(46, 90, 78, 0.35) 0%, transparent 70%); 842 top: 10%; 843 left: -10%; 844 animation: login-blob-float-1 18s ease-in-out infinite; 845 } 846 .login-blob-2 { 847 width: 55vmin; 848 height: 55vmin; 849 background: radial-gradient(circle, rgba(152, 211, 196, 0.5) 0%, transparent 70%); 850 top: 50%; 851 right: -15%; 852 animation: login-blob-float-2 22s ease-in-out infinite; 853 } 854 .login-blob-3 { 855 width: 45vmin; 856 height: 45vmin; 857 background: radial-gradient(circle, rgba(46, 90, 78, 0.25) 0%, transparent 70%); 858 bottom: 5%; 859 left: 20%; 860 animation: login-blob-float-3 20s ease-in-out infinite; 861 } 862 .login-blob-4 { 863 width: 60vmin; 864 height: 60vmin; 865 background: radial-gradient(circle, rgba(184, 221, 214, 0.45) 0%, transparent 70%); 866 top: 30%; 867 left: 30%; 868 animation: login-blob-float-4 25s ease-in-out infinite; 869 } 870 .login-blob-5 { 871 width: 40vmin; 872 height: 40vmin; 873 background: radial-gradient(circle, rgba(46, 90, 78, 0.2) 0%, transparent 70%); 874 bottom: 25%; 875 right: 10%; 876 animation: login-blob-float-5 16s ease-in-out infinite; 877 } 878 @keyframes login-blob-float-1 { 879 0%, 100% { transform: translate(0, 0) scale(1); } 880 33% { transform: translate(15%, -10%) scale(1.05); } 881 66% { transform: translate(-10%, 15%) scale(0.95); } 882 } 883 @keyframes login-blob-float-2 { 884 0%, 100% { transform: translate(0, 0) scale(1); } 885 33% { transform: translate(-20%, 5%) scale(1.1); } 886 66% { transform: translate(10%, -15%) scale(0.9); } 887 } 888 @keyframes login-blob-float-3 { 889 0%, 100% { transform: translate(0, 0) scale(1); } 890 50% { transform: translate(-5%, -20%) scale(1.08); } 891 } 892 @keyframes login-blob-float-4 { 893 0%, 100% { transform: translate(0, 0) scale(1); } 894 25% { transform: translate(10%, 10%) scale(0.92); } 895 75% { transform: translate(-15%, -5%) scale(1.06); } 896 } 897 @keyframes login-blob-float-5 { 898 0%, 100% { transform: translate(0, 0) scale(1); } 899 50% { transform: translate(20%, 10%) scale(1.12); } 900 } 901 .login-mobile-logo-wrap { 902 position: absolute; 903 top: 50%; 904 left: 50%; 905 transform: translate(-50%, -50%); 906 z-index: 1; 907 width: 120px; 908 height: 120px; 909 display: flex; 910 align-items: center; 911 justify-content: center; 912 border-radius: 24px; 913 background: rgba(255, 255, 255, 0.5); 914 backdrop-filter: blur(12px); 915 box-shadow: 0 8px 32px rgba(46, 90, 78, 0.15); 916 } 917 .login-mobile-logo { 918 width: 80px; 919 height: 80px; 920 object-fit: contain; 921 } 922 .login-page-container { 923 position: relative; 924 z-index: 1; 925 } 926 .login-video-col { 927 display: none; 928 } 929} 930</style>
931<script> 932/* Load video only on desktop so mobile never fetches it */ 933(function() { 934 var video = document.querySelector('.login-page-container .bg-video'); 935 var source = video && video.querySelector('source[data-src]'); 936 if (!video || !source) return; 937 if (window.matchMedia && window.matchMedia('(min-width: 768px)').matches) { 938 source.src = source.getAttribute('data-src'); 939 video.load(); 940 video.play().catch(function() {}); 941 } 942})(); 943</script>
943
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.