import { t, applyTranslations } from "./i18n.js"; // Analytics tracking helper function track(eventName, eventData = {}) { if (typeof umami !== "undefined" && umami.track) { umami.track(eventName, eventData); } } let currentUser = null; let oauthHandled = false; let lessonEngineRef = null; let authModule = null; let progressModule = null; let supabaseAvailable = false; /** * Check for OAuth callback tokens in URL hash BEFORE router runs. * Call this before initializing the router. * @returns {Promise} true if OAuth callback was detected and handled */ export async function handleOAuthCallback() { const hash = window.location.hash; // Check if hash contains OAuth tokens (access_token, error, etc.) if (!hash.includes("access_token") && !hash.includes("error_description") && !hash.includes("refresh_token")) { return false; } try { const supabaseModule = await import("./supabase.js"); if (!supabaseModule.isConfigured) { return false; } // Parse tokens from hash const params = new URLSearchParams(hash.substring(1)); const accessToken = params.get("access_token"); const refreshToken = params.get("refresh_token"); if (accessToken && refreshToken) { // Explicitly set the session with tokens from URL const { data, error } = await supabaseModule.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }); if (!error && data?.session) { oauthHandled = true; const provider = data.session.user?.app_metadata?.provider || "oauth"; track("auth_login", { method: provider }); } } // Restore the original route (saved before OAuth redirect) const savedRoute = localStorage.getItem("codeCrispies.oauthReturnRoute"); if (savedRoute) { localStorage.removeItem("codeCrispies.oauthReturnRoute"); window.history.replaceState(null, "", window.location.pathname + savedRoute); } else { // No saved route - just clear the hash window.history.replaceState(null, "", window.location.pathname); } return true; } catch (e) { console.error("[Auth] OAuth callback handling failed:", e.message); return false; } } /** * Initialize the auth system * @param {Object} engine - The LessonEngine instance */ export async function initAuth(engine) { lessonEngineRef = engine; // Try to load Supabase - if not configured, auth is disabled try { const supabaseModule = await import("./supabase.js"); // Check if Supabase is configured via environment variables if (!supabaseModule.isConfigured) { console.log("Supabase not configured - auth disabled"); hideAuthUI(); return; } authModule = supabaseModule.auth; progressModule = supabaseModule.progressDB; supabaseAvailable = true; } catch (e) { console.log("Supabase not available - auth disabled:", e.message); hideAuthUI(); return; } // Listen for auth changes authModule.onAuthStateChange((event, session) => { if (event === "SIGNED_IN" && session?.user) { handleLogin(session.user); } else if (event === "SIGNED_OUT") { handleLogout(); } }); // Check initial session try { const { data } = await authModule.getSession(); if (data?.session?.user) handleLogin(data.session.user); } catch (e) { // Session check failed - continue without auth } // Attach form handlers setupAuthForms(); } function hideAuthUI() { document.getElementById("auth-trigger-header")?.classList.add("hidden"); document.querySelector(".sidebar-auth-box")?.classList.add("hidden"); } async function handleLogin(user) { currentUser = user; updateAuthUI(user); if (!progressModule) return; // Load cloud progress const { data } = await progressModule.load(user.id); if (data) { // Merge with localStorage (cloud wins for conflicts) mergeProgress(data); } else { // First login: upload localStorage to cloud await syncToCloud(); } } function handleLogout() { currentUser = null; updateAuthUI(null); // Keep localStorage progress, just disconnect from cloud } function updateAuthUI(user) { // Header elements const authTriggerHeader = document.getElementById("auth-trigger-header"); const userEmailHeader = document.getElementById("user-email-header"); // Sidebar elements const authTriggerSidebar = document.getElementById("auth-trigger-sidebar"); const authTriggerMobile = document.getElementById("auth-trigger-mobile"); const userMenuSidebar = document.getElementById("user-menu-sidebar"); const userEmailSidebar = document.getElementById("user-email-sidebar"); const sidebarHint = document.querySelector(".sidebar-auth-hint"); if (user) { authTriggerHeader?.classList.add("hidden"); userEmailHeader?.classList.remove("hidden"); authTriggerSidebar?.classList.add("hidden"); authTriggerMobile?.classList.add("hidden"); userMenuSidebar?.classList.remove("hidden"); sidebarHint?.classList.add("hidden"); if (userEmailHeader) userEmailHeader.textContent = user.email; if (userEmailSidebar) userEmailSidebar.textContent = user.email; } else { authTriggerHeader?.classList.remove("hidden"); userEmailHeader?.classList.add("hidden"); authTriggerSidebar?.classList.remove("hidden"); authTriggerMobile?.classList.remove("hidden"); userMenuSidebar?.classList.add("hidden"); sidebarHint?.classList.remove("hidden"); } } export async function syncToCloud() { if (!currentUser || !progressModule) return; const progress = JSON.parse( localStorage.getItem("codeCrispies.progress") || "{}" ); const userCodeEntries = JSON.parse( localStorage.getItem("codeCrispies.userCode") || "[]" ); const userCode = Object.fromEntries(userCodeEntries); const settings = JSON.parse( localStorage.getItem("codeCrispies.settings") || "{}" ); const language = localStorage.getItem("codeCrispies.language") || "en"; await progressModule.save(currentUser.id, progress, userCode, settings, language); } function mergeProgress(cloudData) { // Update localStorage with cloud data localStorage.setItem( "codeCrispies.progress", JSON.stringify(cloudData.progress) ); localStorage.setItem( "codeCrispies.userCode", JSON.stringify(Object.entries(cloudData.user_code)) ); localStorage.setItem( "codeCrispies.settings", JSON.stringify(cloudData.settings) ); localStorage.setItem("codeCrispies.language", cloudData.language); // Reload engine state if (lessonEngineRef) { lessonEngineRef.loadUserProgress(); lessonEngineRef.loadUserCodeFromStorage(); } } export function isLoggedIn() { return supabaseAvailable && currentUser !== null; } export function getCurrentUser() { return currentUser; } // Debounce utility function debounce(fn, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } // Export debounced sync for use by LessonEngine export const debouncedSyncToCloud = debounce(() => syncToCloud(), 2000); function setupAuthForms() { const authDialog = document.getElementById("auth-dialog"); const loginForm = document.getElementById("login-form"); const signupForm = document.getElementById("signup-form"); const resetForm = document.getElementById("reset-form"); // Form submissions loginForm?.addEventListener("submit", handleLoginSubmit); signupForm?.addEventListener("submit", handleSignupSubmit); resetForm?.addEventListener("submit", handleResetSubmit); // Form switchers document .getElementById("show-signup") ?.addEventListener("click", () => switchForm("signup")); document .getElementById("show-login") ?.addEventListener("click", () => switchForm("login")); document .getElementById("show-reset") ?.addEventListener("click", () => switchForm("reset")); // Dialog triggers (header, sidebar, and mobile) document .getElementById("auth-trigger-header") ?.addEventListener("click", () => { authDialog?.showModal(); }); document .getElementById("auth-trigger-sidebar") ?.addEventListener("click", () => { authDialog?.showModal(); }); document .getElementById("auth-trigger-mobile") ?.addEventListener("click", () => { authDialog?.showModal(); }); // Logout button (sidebar only) document .getElementById("logout-btn-sidebar") ?.addEventListener("click", async () => { await authModule?.signOut(); track("auth_logout"); }); // Delete account button and dialog const deleteDialog = document.getElementById("delete-account-dialog"); document .getElementById("delete-account-btn") ?.addEventListener("click", () => { deleteDialog?.showModal(); }); document .getElementById("cancel-delete") ?.addEventListener("click", () => { deleteDialog?.close(); }); document .getElementById("delete-dialog-close") ?.addEventListener("click", () => { deleteDialog?.close(); }); deleteDialog?.addEventListener("click", (e) => { if (e.target === deleteDialog) deleteDialog.close(); }); document .getElementById("confirm-delete") ?.addEventListener("click", async () => { const errorEl = document.getElementById("delete-account-error"); const confirmBtn = document.getElementById("confirm-delete"); confirmBtn.disabled = true; const { error } = await authModule.deleteAccount(); if (error) { errorEl.textContent = error.message; errorEl.classList.remove("hidden"); confirmBtn.disabled = false; } else { errorEl.classList.add("hidden"); deleteDialog.close(); track("auth_delete_account"); // Sign out and clear local state await authModule.signOut(); } }); // OAuth buttons - save current route before redirect document.getElementById("google-login")?.addEventListener("click", async () => { // Save current route to restore after OAuth const currentHash = window.location.hash; if (currentHash && !currentHash.includes("access_token")) { localStorage.setItem("codeCrispies.oauthReturnRoute", currentHash); } const { error } = await authModule?.signInWithGoogle() ?? { error: null }; if (error) { showOAuthError(error.message); } }); document.getElementById("github-login")?.addEventListener("click", async () => { // Save current route to restore after OAuth const currentHash = window.location.hash; if (currentHash && !currentHash.includes("access_token")) { localStorage.setItem("codeCrispies.oauthReturnRoute", currentHash); } const { error } = await authModule?.signInWithGitHub() ?? { error: null }; if (error) { showOAuthError(error.message); } }); // Close dialog on backdrop click authDialog?.addEventListener("click", (e) => { if (e.target === authDialog) authDialog.close(); }); // Close button authDialog?.querySelector(".close-dialog")?.addEventListener("click", () => { authDialog.close(); }); } async function handleLoginSubmit(e) { e.preventDefault(); const email = document.getElementById("login-email").value; const password = document.getElementById("login-password").value; const errorEl = document.getElementById("login-error"); const submitBtn = e.target.querySelector('button[type="submit"]'); // Disable button while processing submitBtn.disabled = true; const { error } = await authModule.signIn(email, password); submitBtn.disabled = false; if (error) { errorEl.textContent = error.message; errorEl.classList.remove("hidden"); } else { errorEl.classList.add("hidden"); document.getElementById("auth-dialog").close(); track("auth_login", { method: "email" }); } } async function handleSignupSubmit(e) { e.preventDefault(); const email = document.getElementById("signup-email").value; const password = document.getElementById("signup-password").value; const confirm = document.getElementById("signup-confirm").value; const errorEl = document.getElementById("signup-error"); const submitBtn = e.target.querySelector('button[type="submit"]'); if (password !== confirm) { errorEl.textContent = t("authPasswordMismatch") || "Passwords do not match"; errorEl.classList.remove("hidden"); return; } // Disable button while processing submitBtn.disabled = true; const { error } = await authModule.signUp(email, password); submitBtn.disabled = false; if (error) { errorEl.textContent = error.message; errorEl.classList.remove("hidden"); document.getElementById("signup-success")?.classList.add("hidden"); } else { errorEl.classList.add("hidden"); // Show success message const successEl = document.getElementById("signup-success"); successEl?.classList.remove("hidden"); // Hide the form fields and button e.target.querySelectorAll(".form-field, button[type='submit']").forEach(el => { el.classList.add("hidden"); }); track("auth_signup", { method: "email" }); } } async function handleResetSubmit(e) { e.preventDefault(); const email = document.getElementById("reset-email").value; const errorEl = document.getElementById("reset-error"); const successEl = document.getElementById("reset-success"); const submitBtn = e.target.querySelector('button[type="submit"]'); // Disable button while processing submitBtn.disabled = true; const { error } = await authModule.resetPassword(email); submitBtn.disabled = false; if (error) { errorEl.textContent = error.message; errorEl.classList.remove("hidden"); successEl.classList.add("hidden"); } else { errorEl.classList.add("hidden"); successEl.classList.remove("hidden"); } } function showOAuthError(message) { // Show error in the currently visible form's error element const loginError = document.getElementById("login-error"); const signupError = document.getElementById("signup-error"); // Use whichever form is visible const errorEl = !document.getElementById("login-form")?.classList.contains("hidden") ? loginError : signupError; if (errorEl) { errorEl.textContent = message; errorEl.classList.remove("hidden"); } } function switchForm(formName) { const loginForm = document.getElementById("login-form"); const signupForm = document.getElementById("signup-form"); const resetForm = document.getElementById("reset-form"); const showSignup = document.getElementById("show-signup"); const showLogin = document.getElementById("show-login"); const showReset = document.getElementById("show-reset"); const titleEl = document.getElementById("auth-dialog-title"); const socialSection = document.querySelector(".auth-social"); // Hide all forms loginForm?.classList.add("hidden"); signupForm?.classList.add("hidden"); resetForm?.classList.add("hidden"); // Show the selected form if (formName === "login") { loginForm?.classList.remove("hidden"); showSignup?.classList.remove("hidden"); showLogin?.classList.add("hidden"); showReset?.classList.remove("hidden"); socialSection?.classList.remove("hidden"); if (titleEl) titleEl.setAttribute("data-i18n", "authLogin"); } else if (formName === "signup") { signupForm?.classList.remove("hidden"); // Reset signup form to initial state (in case it was showing success) signupForm?.querySelectorAll(".form-field, button[type='submit']").forEach(el => { el.classList.remove("hidden"); }); signupForm?.reset(); showSignup?.classList.add("hidden"); showLogin?.classList.remove("hidden"); showReset?.classList.add("hidden"); socialSection?.classList.remove("hidden"); if (titleEl) titleEl.setAttribute("data-i18n", "authSignUp"); } else if (formName === "reset") { resetForm?.classList.remove("hidden"); showSignup?.classList.add("hidden"); showLogin?.classList.remove("hidden"); showReset?.classList.add("hidden"); socialSection?.classList.add("hidden"); if (titleEl) titleEl.setAttribute("data-i18n", "authResetPassword"); } // Clear error messages document.getElementById("login-error")?.classList.add("hidden"); document.getElementById("signup-error")?.classList.add("hidden"); document.getElementById("reset-error")?.classList.add("hidden"); document.getElementById("reset-success")?.classList.add("hidden"); // Apply translations to updated elements applyTranslations(); }