PageSourceSearch

http://app.cardora.ro/login

html cardora.ro collected 2026-10-02 23:28:45 UTC 38,308 bytes, 943 lines download raw bytes

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.