// Class Of Learners Global Authentication System (Supabase) // --- Shared Login Modal Injection --- // Injects the standard loginMo modal if the page doesn't already have one inline. function injectLoginModal() { if (document.getElementById('loginMo')) return const mo = document.createElement('div') mo.className = 'col-auth-mo' mo.id = 'loginMo' mo.innerHTML = '

Authenticate

Unlock dashboard storage and cloud sync.

' document.body.appendChild(mo) mo.addEventListener('click', function (e) { if (e.target === this) closeMo() }) } // Compatibility bridge: expose openLogin/closeMo globally so page onclick handlers work // even before the page's own inline scripts define them. if (!window.openLogin) { window.openLogin = function () { injectLoginModal() var mo = document.getElementById('loginMo') if (!mo) return mo.classList.add('open') var mt = document.getElementById('moAuthTitle') var mSub = document.getElementById('moAuthSub') var loggedOutPanel = document.getElementById('loggedOutPanel') var loggedInPanel = document.getElementById('loggedInPanel') var mn = document.getElementById('miName') var me = document.getElementById('miEmail') if (window.colUser) { if (mt) mt.textContent = 'Google Account' if (mSub) mSub.textContent = 'Manage your profile and username.' if (loggedOutPanel) loggedOutPanel.style.display = 'none' if (loggedInPanel) loggedInPanel.style.display = 'block' if (mn) mn.value = window.colUser.name if (me) me.value = window.colUser.email || '' } else { if (mt) mt.textContent = 'Authenticate' if (mSub) mSub.textContent = 'Unlock dashboard storage and cloud sync.' if (loggedOutPanel) loggedOutPanel.style.display = 'flex' if (loggedInPanel) loggedInPanel.style.display = 'none' // Render Google Button var container = document.getElementById('gSignInBtnContainer') if (container) { container.innerHTML = '' if (window.AndroidBridge) { container.innerHTML = '' } else if (typeof google !== 'undefined' && google.accounts) { google.accounts.id.renderButton(container, { theme: 'filled_black', size: 'large', type: 'standard', shape: 'rectangular', width: 280 }) } else { container.innerHTML = '' } } } } } if (!window.closeMo) { window.closeMo = function () { var mo = document.getElementById('loginMo') if (mo) mo.classList.remove('open') } } ;(async function () { if (window._colAuthRunning) return window._colAuthRunning = true // 1. Fetch Global Configuration to get Supabase Keys let authConfig = null try { const res = await fetch('config.json?t=' + new Date().getTime()) if (res.ok) { const config = await res.json() if (config.auth && config.auth.url && config.auth.key) { authConfig = config.auth } } } catch (e) { console.warn('[col-auth] Could not load config.json — authentication disabled.') } // 2. Load Supabase SDK if keys exist if (authConfig) { if (typeof supabase === 'undefined') { if (!document.querySelector('script[src*="@supabase"]')) { const script = document.createElement('script') script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2' script.onload = () => initSupabase(authConfig.url, authConfig.key) document.head.appendChild(script) } else { // Script is loading, wait for it document.querySelector('script[src*="@supabase"]').addEventListener('load', () => initSupabase(authConfig.url, authConfig.key)) } } else { initSupabase(authConfig.url, authConfig.key) } } else { window.colUser = null dispatchAuthEvent() } window.handleGoogleOneTap = async (response) => { if (!window.supabaseClient) return try { const { data, error } = await window.supabaseClient.auth.signInWithIdToken({ provider: 'google', token: response.credential }) if (error) throw error } catch (error) { console.error('One Tap Sign-in error:', error.message) } } function initOneTap() { if (window.colUser) return if (typeof google === 'undefined' || !google.accounts) { const script = document.createElement('script') script.src = 'https://accounts.google.com/gsi/client' script.onload = () => setupOneTap() document.head.appendChild(script) } else { setupOneTap() } } function setupOneTap() { if (window.colUser) return google.accounts.id.initialize({ client_id: '500448449044-hv2rp3k0lsok9ara1bred87c75lnsp7l.apps.googleusercontent.com', callback: window.handleGoogleOneTap, use_fedcm_for_prompt: true, itp_support: true }) google.accounts.id.prompt((notification) => { if (notification.isNotDisplayed()) { console.log('One tap not displayed: ', notification.getNotDisplayedReason()) } else if (notification.isSkippedMoment()) { console.log('One tap skipped: ', notification.getSkippedReason()) } else if (notification.isDismissedMoment()) { console.log('One tap dismissed: ', notification.getDismissedReason()) } }) // Re-render button if modal is open var container = document.getElementById('gSignInBtnContainer') if (container && !window.AndroidBridge) { container.innerHTML = '' google.accounts.id.renderButton(container, { theme: 'filled_black', size: 'large', type: 'standard', shape: 'rectangular', width: 280 }) } } let supabaseClient = null function initSupabase(url, key) { supabaseClient = window.supabase.createClient(url, key) window.supabaseClient = supabaseClient // Listen for Auth changes supabaseClient.auth.onAuthStateChange((event, session) => { if (session && session.user) { const meta = session.user.user_metadata || {} window.colUser = { id: session.user.id, email: session.user.email, name: meta.full_name || meta.name || session.user.email.split('@')[0], picture: meta.avatar_url || meta.picture || null, session: session } } else { window.colUser = null } dispatchAuthEvent() updateAuthUI() if (!session && !window._oneTapAttempted && (event === 'INITIAL_SESSION' || event === 'SIGNED_OUT')) { window._oneTapAttempted = true if (event === 'INITIAL_SESSION') { initOneTap() } } }) injectAuthStyles() injectAuthUI() // If there's a custom handler provided by the page (like in qr.html to fetch drive data), fire it. dispatchAuthEvent() } function dispatchAuthEvent() { const event = new CustomEvent('col-auth-changed', { detail: { user: window.colUser } }) window.dispatchEvent(event) } // --- UI INJECTION --- function injectAuthStyles() { if (document.getElementById('col-auth-styles')) return const style = document.createElement('style') style.id = 'col-auth-styles' style.innerHTML = ` .col-auth-mo { position: fixed; inset: 0; background: rgba(4, 7, 14, 0.75); backdrop-filter: blur(16px); z-index: 99999; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: 0.4s cubic-bezier(.16,1,.3,1); } .col-auth-mo.open { opacity: 1; pointer-events: auto; } .col-auth-md { background: var(--panel, #111827); border: 1px solid var(--lineb, rgba(255, 255, 255, 0.12)); border-radius: 28px; width: 90%; max-width: 420px; overflow: hidden; transform: translateY(40px) scale(0.95); transition: 0.5s cubic-bezier(.16,1,.3,1); box-shadow: 0 40px 100px rgba(0,0,0,0.9), inset 0 1px 1px rgba(255,255,255,0.08); color: var(--ink, #E8E3D8); font-family: var(--sans, 'Inter'), sans-serif;} .col-auth-mo.open .col-auth-md { transform: translateY(0) scale(1); } .col-auth-hd { padding: 40px 30px 25px; border-bottom: 1px solid var(--line, rgba(255,255,255,0.06)); background: transparent; text-align: center; position: relative;} .col-auth-close { position: absolute; top: 20px; right: 20px; background: transparent; border: 1px solid var(--lineb, rgba(255,255,255,0.1)); color: var(--dim, #8891AA); font-size: 1.2rem; cursor: pointer; transition: 0.3s; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center;} .col-auth-close:hover { color: var(--ink, #E8E3D8); background: var(--line, rgba(255,255,255,0.1)); border-color: var(--lineb, rgba(255,255,255,0.2)); transform: scale(1.05); } .col-auth-hd h2 { font-size: 2.6rem; font-weight: 400; font-family: var(--serif, 'Instrument Serif'); font-style: italic; margin:0; color: var(--page-theme, var(--signal, #F2B84B)); letter-spacing: 0.5px;} .col-auth-hd p { font-size: 0.95rem; color: var(--dim, #8891AA); margin-top: 8px; } .col-auth-body { padding: 30px; } .col-auth-inp { width: 100%; padding: 14px 16px; border: 1px solid var(--lineb, rgba(255,255,255,0.1)); border-radius: 14px; font-size: 1rem; outline: none; margin-bottom: 16px; transition: 0.3s; color: var(--ink, #E8E3D8); background: var(--void2, rgba(255, 255, 255, 0.03)); font-family: var(--sans, 'Inter'), sans-serif;} .col-auth-inp:focus { border-color: var(--page-theme, var(--signal, #F2B84B)); box-shadow: 0 0 0 4px var(--line, rgba(255,255,255,0.1)); background: var(--void, rgba(255, 255, 255, 0.05)); } .col-auth-btn { width: 100%; padding: 14px; background: var(--page-theme, var(--signal, #F2B84B)); border: none; border-radius: 14px; font-weight: 700; font-size: 1rem; letter-spacing: 0.5px; cursor: pointer; color: var(--void, #070A14); transition: 0.3s cubic-bezier(.16,1,.3,1); font-family: var(--sans, 'Inter'), sans-serif;} .col-auth-btn:hover { box-shadow: 0 12px 24px rgba(242,184,75,0.2); transform: translateY(-2px); opacity: 0.9; } .col-auth-danger { width: 100%; padding: 14px; background: transparent; border: 1px solid rgba(239, 68, 68, 0.5); border-radius: 14px; font-weight: 700; font-size: 1rem; cursor: pointer; color: #ef4444; transition: 0.3s; font-family: var(--sans, 'Inter'), sans-serif;} .col-auth-danger:hover { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; } .col-auth-gbtn { width: 100%; padding: 14px; background: var(--ink, #ffffff); border: none; border-radius: 14px; font-weight: 600; font-size: 1rem; cursor: pointer; color: var(--void, #0f1419); display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 24px; transition: 0.3s cubic-bezier(.16,1,.3,1); box-shadow: 0 4px 12px rgba(0,0,0,0.1); font-family: var(--sans, 'Inter'), sans-serif;} .col-auth-gbtn:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(0,0,0,0.2); opacity: 0.9; } .col-auth-div { display: flex; align-items: center; text-align: center; color: var(--dim, #8891AA); font-size: 0.8rem; margin-bottom: 24px; font-weight: 600; font-family: var(--mono, 'Space Mono'), monospace; letter-spacing: 2px;} .col-auth-div::before, .col-auth-div::after { content: ''; flex: 1; border-bottom: 1px solid var(--line, rgba(255,255,255,0.06)); } .col-auth-div:not(:empty)::before { margin-right: 1em; } .col-auth-div:not(:empty)::after { margin-left: 1em; } .col-auth-tab-group { display: flex; gap: 6px; margin-bottom: 24px; background: var(--void2, rgba(255,255,255,0.03)); border: 1px solid var(--line, rgba(255,255,255,0.06)); border-radius: 16px; padding: 6px;} .col-auth-tab { flex: 1; text-align: center; padding: 10px; border-radius: 12px; font-size: 0.9rem; font-weight: 600; color: var(--dim, #8891AA); cursor: pointer; transition: 0.3s;} .col-auth-tab:hover:not(.active) { color: var(--ink, #E8E3D8); background: var(--line, rgba(255,255,255,0.05)); } .col-auth-tab.active { background: var(--line, rgba(255,255,255,0.05)); color: var(--page-theme, var(--signal, #F2B84B)); box-shadow: 0 2px 8px rgba(0,0,0,0.2); } /* Override existing nav profile logic for global injection */ .nav-login-btn { display: flex; align-items: center; gap: 8px; background: transparent; color: var(--page-theme, var(--signal, #F2B84B)); border: 1px solid var(--page-theme, var(--signal, #F2B84B)); padding: 8px 20px; border-radius: 30px; font-weight: 800; font-size: 0.85rem; cursor: pointer; transition: 0.3s; text-decoration:none; } body.lm .nav-login-btn { background: transparent; color: var(--page-theme-light, var(--signal, #F2B84B)); border-color: var(--page-theme-light, var(--signal, #F2B84B)); } body.lm .nav-login-btn:hover { background: var(--page-theme-light, var(--signal, #F2B84B)); color: #fff; } .nav-login-btn:hover { background: var(--page-theme, var(--signal, #F2B84B)); color: var(--void, #070A14); box-shadow: 0 0 20px rgba(242,184,75,0.2); } .nav-user-profile { display: none; align-items: center; gap: 10px; padding: 5px 15px 5px 5px; background: transparent; border: 1px solid var(--lineb, rgba(255,255,255,.16)); border-radius: 30px; cursor: pointer; transition: 0.3s; } .nav-user-profile:hover { border-color: var(--page-theme, var(--signal, #F2B84B)); } .nav-user-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--page-theme, var(--signal, #F2B84B)); color: var(--void, #070A14); display: flex; justify-content: center; align-items: center; font-weight: 800; overflow: hidden; } ` document.head.appendChild(style) } function injectAuthUI() { if (document.getElementById('colAuthModal')) return const modal = document.createElement('div') modal.className = 'col-auth-mo' modal.id = 'colAuthModal' modal.setAttribute('role', 'dialog') modal.setAttribute('aria-modal', 'true') modal.setAttribute('aria-labelledby', 'colAuthTitle') modal.innerHTML = `

Authenticate

Unlock dashboard storage and cloud sync.

` document.body.appendChild(modal) // Expose global open function window.openGlobalLogin = function () { renderAuthPanel() var mo = document.getElementById('colAuthModal') mo.classList.add('open') // Focus first focusable element setTimeout(function(){ var f = mo.querySelector('button, input, [tabindex]:not([tabindex="-1"])'); if(f) f.focus(); }, 100) } // Escape key closes modal document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ var mo = document.getElementById('colAuthModal') if(mo && mo.classList.contains('open')) mo.classList.remove('open') } }) // Focus trap modal.addEventListener('keydown', function(e){ if(e.key!=='Tab') return var focusable = modal.querySelectorAll('button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])') if(!focusable.length) return var first = focusable[0], last = focusable[focusable.length-1] if(e.shiftKey){ if(document.activeElement===first){ e.preventDefault(); last.focus(); } } else { if(document.activeElement===last){ e.preventDefault(); first.focus(); } } }) } function renderAuthPanel(tab = 'login') { const body = document.getElementById('colAuthBody') if (!body) return if (window.colUser) { body.innerHTML = `
${window.colUser.picture ? `` : (window.colUser.name || '?').charAt(0).toUpperCase()}

${window.colUser.name}

${window.colUser.email}

` return } const isLogin = tab === 'login' body.innerHTML = `
OR EMAIL
Sign In
Create Account
${!isLogin ? `` : ''}
` } // Expose helpers for inline handlers window._renderAuthTab = (tab) => renderAuthPanel(tab) window.colDoGoogle = async () => { if (window.AndroidBridge) { window.AndroidBridge.signInWithGoogle() return } if (!supabaseClient) return // Restrict redirect to known production origin to prevent open redirect var allowedOrigin = 'https://advancedlogiclabs.dpdns.org' var redirectUrl = window.location.origin === allowedOrigin ? window.location.href : allowedOrigin await supabaseClient.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: redirectUrl } }) } window._handleColAuthSubmit = async (e, mode) => { e.preventDefault() if (!supabaseClient) return const email = document.getElementById('colAuthEmail').value const pass = document.getElementById('colAuthPass').value const btn = document.getElementById('colAuthSubmitBtn') const errDiv = document.getElementById('colAuthError') btn.textContent = 'Processing...' btn.disabled = true errDiv.style.display = 'none' try { let res if (mode === 'signup') { const name = document.getElementById('colAuthName').value res = await supabaseClient.auth.signUp({ email: email, password: pass, options: { data: { full_name: name } } }) } else { res = await supabaseClient.auth.signInWithPassword({ email: email, password: pass }) } if (res.error) throw res.error if (mode === 'signup' && res.data.user && !res.data.session) { errDiv.textContent = 'Please check your email to confirm registration.' errDiv.style.color = '#10b981' errDiv.style.display = 'block' } else { document.getElementById('colAuthModal').classList.remove('open') } } catch (error) { errDiv.textContent = error.message errDiv.style.color = '#ef4444' errDiv.style.display = 'block' } btn.textContent = mode === 'login' ? 'Sign In' : 'Create Account' btn.disabled = false } window.colDoLogout = async () => { if (!supabaseClient) return await supabaseClient.auth.signOut() document.getElementById('colAuthModal').classList.remove('open') } // --- APK Certificate Verification --- window.colApkVerified = false window.colApkFingerprint = null async function verifyApkCertificate() { if (!window.AndroidBridge) { window.colApkVerified = false window.dispatchEvent(new CustomEvent('col-apk-verified', { detail: { verified: false } })) return } try { const fp = window.AndroidBridge.getCertificateFingerprint() if (!fp) { window.colApkVerified = false window.dispatchEvent(new CustomEvent('col-apk-verified', { detail: { verified: false } })) return } window.colApkFingerprint = fp // Fetch expected fingerprint from config let expected = null try { const res = await fetch('config.json?t=' + Date.now()) if (res.ok) { const cfg = await res.json() expected = cfg.apkCertFingerprint || null } } catch (e) {} if (expected && fp.toUpperCase() === expected.toUpperCase()) { window.colApkVerified = true } else { window.colApkVerified = false } window.dispatchEvent(new CustomEvent('col-apk-verified', { detail: { verified: window.colApkVerified, fingerprint: fp } })) } catch (e) { console.warn('[col-auth] APK verification failed:', e) window.colApkVerified = false window.dispatchEvent(new CustomEvent('col-apk-verified', { detail: { verified: false } })) } } verifyApkCertificate() function updateAuthUI() { // Find existing UI elements in any page const navBtns = document.querySelectorAll('.nav-login-btn, #navLoginBtn, #sbSignBtn') const navProfiles = document.querySelectorAll('.nav-user-profile, #navUserProfile, #pCard') if (window.colUser) { navBtns.forEach((btn) => (btn.style.display = 'none')) navProfiles.forEach((prof) => { prof.style.display = 'flex' if (prof.dataset.preserveClick !== 'true') { prof.onclick = function () { if (window.openGlobalLogin) window.openGlobalLogin() else if (window.openLogin) window.openLogin() } } // Try to find the inner text elements const nameEls = prof.querySelectorAll('span, .pname') const avEls = prof.querySelectorAll('.nav-user-avatar, .pav') const emailEls = prof.querySelectorAll('.pemail') nameEls.forEach((el) => (el.textContent = window.colUser.name.split(' ')[0])) emailEls.forEach((el) => (el.textContent = window.colUser.email)) avEls.forEach((av) => { if (window.colUser.picture) { av.innerHTML = `` } else { av.innerHTML = '' av.textContent = window.colUser.name.charAt(0).toUpperCase() } }) }) } else { navBtns.forEach((btn) => { btn.style.display = 'flex' btn.onclick = function () { if (window.openGlobalLogin) window.openGlobalLogin() else if (window.openLogin) window.openLogin() } }) navProfiles.forEach((prof) => (prof.style.display = 'none')) } } // Auto-update UI on load if we have cached elements but Auth hasn't finished loading yet document.addEventListener('DOMContentLoaded', updateAuthUI) })()