/** * OxiCloud Authentication JavaScript * Handles login, registration, and admin setup */ // API endpoints const API_URL = '/api/auth'; const LOGIN_ENDPOINT = `${API_URL}/login`; const REGISTER_ENDPOINT = `${API_URL}/register`; const ME_ENDPOINT = `${API_URL}/me`; const REFRESH_ENDPOINT = `${API_URL}/refresh`; // Storage keys const TOKEN_KEY = 'oxicloud_token'; const REFRESH_TOKEN_KEY = 'oxicloud_refresh_token'; const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry'; const USER_DATA_KEY = 'oxicloud_user'; const LOCALE_KEY = 'oxicloud-locale'; const FIRST_RUN_KEY = 'oxicloud_first_run_completed'; // Language selector texts (used before i18n is loaded) const LANGUAGE_TEXTS = { en: { title: 'Welcome to OxiCloud', subtitle: 'Please select your language', continue: 'Continue', autodetected: 'We detected your language', moreLanguages: 'More languages...', modalTitle: 'Select language', searchPlaceholder: 'Search language...' }, es: { title: 'Bienvenido a OxiCloud', subtitle: 'Por favor, selecciona tu idioma', continue: 'Continuar', autodetected: 'Hemos detectado tu idioma', moreLanguages: 'More languages...', modalTitle: 'Seleccionar idioma', searchPlaceholder: 'Buscar idioma...' }, zh: { title: '欢迎使用 OxiCloud', subtitle: '请选择您的语言', continue: '继续', autodetected: '我们检测到了您的语言', moreLanguages: '更多语言...', modalTitle: '选择语言', searchPlaceholder: '搜索语言...' }, fa: { title: 'به OxiCloud خوش آمدید', subtitle: 'لطفا زبان خود را انتخاب کنید', continue: 'ادامه', autodetected: 'زبان شما شناسایی شد', moreLanguages: 'زبان‌های بیشتر...', modalTitle: 'انتخاب زبان', searchPlaceholder: 'جستجوی زبان...' } }; // Complete language registry — add new languages here, they'll appear automatically // `popular: true` languages show as cards on the main screen, the rest in the modal const ALL_LANGUAGES = [ { code: 'en', name: 'English', nativeName: 'English', flag: '🇬🇧', popular: true }, { code: 'es', name: 'Spanish', nativeName: 'Español', flag: '🇪🇸', popular: true }, { code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳', popular: true }, { code: 'fa', name: 'Persian', nativeName: 'فارسی', flag: '🇮🇷', popular: true }, { code: 'fr', name: 'French', nativeName: 'Français', flag: '🇫🇷', popular: true }, { code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', popular: true }, { code: 'pt', name: 'Portuguese', nativeName: 'Português', flag: '🇧🇷', popular: true }, { code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹', popular: true }, { code: 'ru', name: 'Russian', nativeName: 'Русский', flag: '🇷🇺', popular: false }, { code: 'ja', name: 'Japanese', nativeName: '日本語', flag: '🇯🇵', popular: false }, { code: 'ko', name: 'Korean', nativeName: '한국어', flag: '🇰🇷', popular: false }, { code: 'ar', name: 'Arabic', nativeName: 'العربية', flag: '🇸🇦', popular: false }, { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳', popular: false }, { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', flag: '🇹🇷', popular: false }, { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', flag: '🇳🇱', popular: false }, { code: 'pl', name: 'Polish', nativeName: 'Polski', flag: '🇵🇱', popular: false }, { code: 'sv', name: 'Swedish', nativeName: 'Svenska', flag: '🇸🇪', popular: false }, { code: 'da', name: 'Danish', nativeName: 'Dansk', flag: '🇩🇰', popular: false }, { code: 'fi', name: 'Finnish', nativeName: 'Suomi', flag: '🇫🇮', popular: false }, { code: 'no', name: 'Norwegian', nativeName: 'Norsk', flag: '🇳🇴', popular: false }, { code: 'uk', name: 'Ukrainian', nativeName: 'Українська', flag: '🇺🇦', popular: false }, { code: 'cs', name: 'Czech', nativeName: 'Čeština', flag: '🇨🇿', popular: false }, { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', flag: '🇬🇷', popular: false }, { code: 'he', name: 'Hebrew', nativeName: 'עברית', flag: '🇮🇱', popular: false }, { code: 'th', name: 'Thai', nativeName: 'ไทย', flag: '🇹🇭', popular: false }, { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', flag: '🇻🇳', popular: false }, { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', flag: '🇮🇩', popular: false }, { code: 'ms', name: 'Malay', nativeName: 'Bahasa Melayu', flag: '🇲🇾', popular: false }, { code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴', popular: false }, { code: 'hu', name: 'Hungarian', nativeName: 'Magyar', flag: '🇭🇺', popular: false }, { code: 'ca', name: 'Catalan', nativeName: 'Català', flag: '🏴', popular: false }, { code: 'eu', name: 'Basque', nativeName: 'Euskara', flag: '🏴', popular: false }, { code: 'gl', name: 'Galician', nativeName: 'Galego', flag: '🏴', popular: false }, ]; // Check if this is a first run (no locale saved) function isFirstRun() { return !localStorage.getItem(LOCALE_KEY); } // Check system status from the server async function checkSystemStatus() { try { const response = await fetch('/api/auth/status'); if (!response.ok) { console.warn('Could not check system status, assuming initialized'); return { initialized: true, admin_count: 1, registration_allowed: true }; } return await response.json(); } catch (error) { console.error('Error checking system status:', error); return { initialized: true, admin_count: 1, registration_allowed: true }; } } // Detect user's browser language and return the best matching language from ALL_LANGUAGES function detectBrowserLanguage() { const browserLangs = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; for (const bl of browserLangs) { const code = bl.substring(0, 2).toLowerCase(); const match = ALL_LANGUAGES.find(l => l.code === code); if (match) return match; } return ALL_LANGUAGES[0]; // fallback to English } // Build a language option element (card style) function buildLanguageCard(lang, isSelected) { const item = document.createElement('div'); item.className = 'lang-picker-item' + (isSelected ? ' selected' : ''); item.setAttribute('data-lang', lang.code); item.setAttribute('role', 'option'); item.setAttribute('aria-selected', isSelected); item.innerHTML = ` ${lang.flag} ${lang.nativeName} ${lang.name} ${isSelected ? '' : ''} `; return item; } // Initialize language selector panel with compact dropdown approach function initLanguageSelector() { const languagePanel = document.getElementById('language-panel'); const continueBtn = document.getElementById('language-continue'); const picker = document.getElementById('lang-picker'); const pickerSelected = document.getElementById('lang-picker-selected'); const pickerDropdown = document.getElementById('lang-picker-dropdown'); const pickerList = document.getElementById('lang-picker-list'); const pickerFlag = document.getElementById('lang-picker-flag'); const pickerName = document.getElementById('lang-picker-name'); const searchInput = document.getElementById('lang-picker-search-input'); if (!languagePanel || !picker) return; // --- Auto-detect browser language --- const detected = detectBrowserLanguage(); let selectedLanguage = detected.code; // Update the selected box with detected language pickerFlag.textContent = detected.flag; pickerName.textContent = detected.nativeName; updateLanguagePanelTexts(selectedLanguage); // Render dropdown list function renderDropdownList(filter = '') { pickerList.innerHTML = ''; const filterLower = filter.toLowerCase(); const filtered = ALL_LANGUAGES.filter(lang => { if (!filter) return true; return lang.name.toLowerCase().includes(filterLower) || lang.nativeName.toLowerCase().includes(filterLower) || lang.code.toLowerCase().includes(filterLower); }); if (filtered.length === 0) { pickerList.innerHTML = '
—
'; return; } filtered.forEach(lang => { const item = buildLanguageCard(lang, lang.code === selectedLanguage); item.addEventListener('click', (e) => { e.stopPropagation(); selectedLanguage = lang.code; pickerFlag.textContent = lang.flag; pickerName.textContent = lang.nativeName; updateLanguagePanelTexts(lang.code); closePicker(); renderDropdownList(''); }); pickerList.appendChild(item); }); } function openPicker() { picker.classList.add('open'); pickerSelected.setAttribute('aria-expanded', 'true'); renderDropdownList(''); if (searchInput) { searchInput.value = ''; setTimeout(() => searchInput.focus(), 50); } // Scroll active item into view setTimeout(() => { const active = pickerList.querySelector('.lang-picker-item.selected'); if (active) active.scrollIntoView({ block: 'nearest' }); }, 60); } function closePicker() { picker.classList.remove('open'); pickerSelected.setAttribute('aria-expanded', 'false'); if (searchInput) searchInput.value = ''; } // Toggle dropdown pickerSelected.addEventListener('click', (e) => { e.stopPropagation(); if (picker.classList.contains('open')) { closePicker(); } else { openPicker(); } }); // Keyboard support pickerSelected.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (picker.classList.contains('open')) closePicker(); else openPicker(); } else if (e.key === 'Escape') { closePicker(); } }); // Search input if (searchInput) { searchInput.addEventListener('input', () => renderDropdownList(searchInput.value)); searchInput.addEventListener('click', (e) => e.stopPropagation()); } // Close when clicking outside document.addEventListener('click', (e) => { if (!picker.contains(e.target)) closePicker(); }); // --- Continue button --- continueBtn.addEventListener('click', async () => { if (!selectedLanguage) return; // Save locale preference localStorage.setItem(LOCALE_KEY, selectedLanguage); localStorage.setItem(FIRST_RUN_KEY, 'true'); // Update i18n if available if (window.i18n && window.i18n.setLocale) { await window.i18n.setLocale(selectedLanguage); } // Hide language panel languagePanel.style.display = 'none'; // Check system status to determine which panel to show const systemStatus = await checkSystemStatus(); console.log('System status after language selection:', systemStatus); if (!systemStatus.initialized) { console.log('No admin exists, showing admin setup panel'); document.getElementById('login-panel').style.display = 'none'; document.getElementById('register-panel').style.display = 'none'; document.getElementById('admin-setup-panel').style.display = 'block'; const backToLoginLink = document.getElementById('back-to-login'); if (backToLoginLink) { backToLoginLink.parentElement.style.display = 'none'; } } else { document.getElementById('login-panel').style.display = 'block'; // Configure OIDC login UI if SSO is enabled await configureOidcLoginUI(); } // Translate the page with new locale if (window.i18n && window.i18n.translatePage) { window.i18n.translatePage(); } }); } // Update language panel texts based on selected language function updateLanguagePanelTexts(lang) { const texts = LANGUAGE_TEXTS[lang] || LANGUAGE_TEXTS.en; const titleEl = document.getElementById('language-title'); const subtitleEl = document.getElementById('language-subtitle'); const continueBtn = document.getElementById('language-continue'); const searchInput = document.getElementById('lang-picker-search-input'); if (titleEl) titleEl.textContent = texts.title; if (subtitleEl) subtitleEl.textContent = texts.subtitle; if (continueBtn) continueBtn.textContent = texts.continue; if (searchInput) searchInput.placeholder = texts.searchPlaceholder; } // Show appropriate panel based on system status and first run async function showInitialPanel() { const languagePanel = document.getElementById('language-panel'); const loginPanel = document.getElementById('login-panel'); const adminSetupPanel = document.getElementById('admin-setup-panel'); const registerPanel = document.getElementById('register-panel'); if (!languagePanel || !loginPanel) return; // ALWAYS check if this is user's first run (language selection) FIRST // Language selection should happen before anything else if (isFirstRun()) { // First run - show language selector first // After language is selected, the continue button handler will check system status console.log('First run - showing language selector'); languagePanel.style.display = 'block'; loginPanel.style.display = 'none'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'none'; return; } // Language already selected - now check system status const systemStatus = await checkSystemStatus(); console.log('System status:', systemStatus); if (!systemStatus.initialized) { // No admin exists - this is a fresh install, show admin setup console.log('Fresh install detected - showing admin setup'); languagePanel.style.display = 'none'; loginPanel.style.display = 'none'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'block'; // Hide the "Already set up? Sign in" link since there's no admin yet const backToLoginLink = document.getElementById('back-to-login'); if (backToLoginLink) { backToLoginLink.parentElement.style.display = 'none'; } return; } // System is initialized - show login panel languagePanel.style.display = 'none'; loginPanel.style.display = 'block'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'none'; // Hide the admin setup link if admin already exists const showAdminSetupLink = document.getElementById('show-admin-setup'); if (showAdminSetupLink && systemStatus.admin_count > 0) { showAdminSetupLink.parentElement.style.display = 'none'; } // Check for OIDC/SSO configuration and update login panel accordingly await configureOidcLoginUI(); } // Fetch OIDC provider info and configure the login UI async function configureOidcLoginUI() { try { const response = await fetch('/api/auth/oidc/providers'); if (!response.ok) return; const oidcInfo = await response.json(); if (!oidcInfo.enabled) return; const oidcSection = document.getElementById('oidc-login-section'); const oidcBtn = document.getElementById('oidc-login-btn'); const loginForm = document.getElementById('login-form'); const authDivider = document.getElementById('auth-divider'); const showRegisterToggle = document.getElementById('show-register'); if (!oidcSection || !oidcBtn) return; // Update button text with provider name const btnTextEl = oidcBtn.querySelector('span'); if (btnTextEl && oidcInfo.provider_name) { const template = (window.i18n && window.i18n.t) ? window.i18n.t('auth.sso_login_provider') : 'Sign in with {{provider}}'; btnTextEl.textContent = template.replace('{{provider}}', oidcInfo.provider_name); } // Redirect to OIDC authorize endpoint on click oidcBtn.addEventListener('click', () => { window.location.href = oidcInfo.authorize_endpoint; }); if (!oidcInfo.password_login_enabled) { // OIDC-only mode: hide password form and divider, show only SSO button if (loginForm) loginForm.style.display = 'none'; if (authDivider) authDivider.style.display = 'none'; if (showRegisterToggle) showRegisterToggle.parentElement.style.display = 'none'; oidcSection.style.display = 'block'; } else { // Both password and OIDC enabled: show divider + SSO button oidcSection.style.display = 'block'; } } catch (err) { console.error('Failed to fetch OIDC provider info:', err); } } // DOM elements let loginPanel, registerPanel, adminSetupPanel, languagePanel; let loginForm, registerForm, adminSetupForm; let loginError, registerError, registerSuccess, adminSetupError; // Initialize DOM elements only if we're on the login page function initLoginElements() { // Check if we're on the login page if (!document.getElementById('login-form')) { console.log('Not on login page, skipping element initialization'); return false; } languagePanel = document.getElementById('language-panel'); loginPanel = document.getElementById('login-panel'); registerPanel = document.getElementById('register-panel'); adminSetupPanel = document.getElementById('admin-setup-panel'); loginForm = document.getElementById('login-form'); registerForm = document.getElementById('register-form'); adminSetupForm = document.getElementById('admin-setup-form'); loginError = document.getElementById('login-error'); registerError = document.getElementById('register-error'); registerSuccess = document.getElementById('register-success'); adminSetupError = document.getElementById('admin-setup-error'); // Initialize language selector initLanguageSelector(); // Panel toggles document.getElementById('show-register').addEventListener('click', () => { loginPanel.style.display = 'none'; registerPanel.style.display = 'block'; adminSetupPanel.style.display = 'none'; }); document.getElementById('show-login').addEventListener('click', () => { loginPanel.style.display = 'block'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'none'; }); document.getElementById('show-admin-setup').addEventListener('click', () => { loginPanel.style.display = 'none'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'block'; }); document.getElementById('back-to-login').addEventListener('click', () => { loginPanel.style.display = 'block'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'none'; }); return true; } // Initialize login elements if on login page const isLoginPage = initLoginElements(); // Check if we already have a valid token let authInitialized = false; // EMERGENCY HANDLER: Detect if page is being loaded from a redirect loop // and clear auth data to break the loop (() => { // Check if we're being redirected in a loop const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0'); const redirectSource = new URLSearchParams(window.location.search).get('source'); // Case 1: High refresh attempts if (refreshAttempts > 3) { console.error('EMERGENCY: Detected severe token refresh loop. Cleaning all auth data.'); localStorage.clear(); // Full localStorage clear to ensure we break the loop sessionStorage.clear(); localStorage.setItem('emergency_clean', 'true'); // Store timestamp of the cleanup for stability localStorage.setItem('last_emergency_clean', Date.now().toString()); // No alert to avoid overwhelming the user if this happens multiple times } // Case 2: We were redirected from app due to auth issues if (redirectSource === 'app') { console.log('Detected redirect from app, ensuring clean auth state'); // Clear only auth-related data to ensure a clean login localStorage.removeItem('oxicloud_token'); localStorage.removeItem('oxicloud_refresh_token'); localStorage.removeItem('oxicloud_token_expiry'); // Reset counters sessionStorage.removeItem('redirect_count'); localStorage.setItem('refresh_attempts', '0'); } // Case 3: Multiple redirects in short time const lastCleanup = parseInt(localStorage.getItem('last_emergency_clean') || '0'); const timeSinceCleanup = Date.now() - lastCleanup; if (lastCleanup > 0 && timeSinceCleanup < 10000) { // Less than 10 seconds console.warn('Multiple auth problems in short time, enabling direct bypass mode'); localStorage.setItem('bypass_auth_mode', 'true'); } })(); document.addEventListener('DOMContentLoaded', () => { // CRITICAL: Stop any potential redirect loops by handling browser throttling if (document.visibilityState === 'hidden') { console.warn('Page hidden, avoiding potential navigation loop'); return; } // Check if we're on the login page if (!document.getElementById('login-form')) { console.log('Not on login page, skipping auth check'); return; } // --- OIDC exchange code handling (fallback if landing on login page) --- const urlParams = new URLSearchParams(window.location.search); const oidcCode = urlParams.get('oidc_code'); if (oidcCode) { console.log('OIDC exchange code detected on login page, exchanging...'); (async () => { try { const resp = await fetch('/api/auth/oidc/exchange', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code: oidcCode }) }); if (!resp.ok) { console.error('OIDC exchange failed:', resp.status); return; // Fall through to normal login page } const data = await resp.json(); const token = data.access_token || data.token; const refreshToken = data.refresh_token || data.refreshToken; if (token) { localStorage.setItem(TOKEN_KEY, token); if (refreshToken) localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); // Parse JWT expiry const tokenParts = token.split('.'); if (tokenParts.length === 3) { try { const payload = JSON.parse(atob(tokenParts[1])); if (payload.exp) { const expiryDate = new Date(payload.exp * 1000); if (!isNaN(expiryDate.getTime())) { localStorage.setItem(TOKEN_EXPIRY_KEY, expiryDate.toISOString()); } } } catch (e) { /* ignore parse errors */ } } if (data.user) { localStorage.setItem(USER_DATA_KEY, JSON.stringify(data.user)); } // Redirect to main app window.location.href = '/?no_redirect=true'; return; } } catch (err) { console.error('OIDC exchange error:', err); } })(); return; // Don't initialize login page while exchanging } if (authInitialized) { console.log('Auth already initialized, skipping'); return; } authInitialized = true; // Show appropriate panel (language selector on first run, login otherwise, or admin setup if no admin) // This is async so we call it and let it run showInitialPanel().then(() => { console.log('Initial panel shown based on system status'); }).catch(err => { console.error('Error showing initial panel:', err); }); // Always clear counters when loading the login page // to ensure we don't get trapped in a loop console.log('Login page loaded, clearing all counters'); sessionStorage.removeItem('redirect_count'); localStorage.removeItem('refresh_attempts'); (async () => { try { // First check if the token is valid const token = localStorage.getItem(TOKEN_KEY); const tokenExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY); if (!token) { console.log('No token found, user needs to login'); // Clear any stale data localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(TOKEN_EXPIRY_KEY); localStorage.removeItem(USER_DATA_KEY); return; // Stay on login page } // Check if token expiry is valid and not expired try { const expiryDate = new Date(tokenExpiry); if (!isNaN(expiryDate.getTime()) && expiryDate > new Date()) { console.log(`Token valid until ${expiryDate.toLocaleString()}`); // Token still valid, redirect to main app redirectToMainApp(); return; } else { console.log('Token expired or invalid date, attempting refresh'); } } catch (dateError) { console.error('Error parsing token expiry date:', dateError); // Continue to refresh attempt } // Token expired, try to refresh const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); if (refreshToken) { try { console.log('Attempting to refresh expired token'); await refreshAuthToken(refreshToken); console.log('Token refresh successful, redirecting to app'); redirectToMainApp(); } catch (error) { // Refresh failed, continue with login page console.log('Token refresh failed, user needs to login again:', error.message); // Clear any stale auth data localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(TOKEN_EXPIRY_KEY); localStorage.removeItem(USER_DATA_KEY); } } else { console.log('No refresh token found, user needs to login'); } // Check if admin account exists (customize this as needed) const isFirstRun = await checkFirstRun(); if (isFirstRun) { loginPanel.style.display = 'none'; registerPanel.style.display = 'none'; adminSetupPanel.style.display = 'block'; } } catch (error) { console.error('Authentication check failed:', error); } })(); }); // Login form submission if (isLoginPage && loginForm) { loginForm.addEventListener('submit', async (e) => { e.preventDefault(); // Clear previous errors loginError.style.display = 'none'; const username = document.getElementById('login-username').value; const password = document.getElementById('login-password').value; try { const data = await login(username, password); // Store auth data console.log("Login response:", data); // Log the response for debugging // Use the correct field names from our API response const token = data.access_token || data.token || "mock_access_token"; const refreshToken = data.refresh_token || data.refreshToken || "mock_refresh_token"; localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); // Extract expiration date from the JWT token let parsedExpiry = false; const tokenParts = token.split('.'); if (tokenParts.length === 3) { try { const payload = JSON.parse(atob(tokenParts[1])); if (payload.exp) { // payload.exp is in seconds since epoch const expiryDate = new Date(payload.exp * 1000); // Verify the date is valid if (!isNaN(expiryDate.getTime())) { localStorage.setItem(TOKEN_EXPIRY_KEY, expiryDate.toISOString()); parsedExpiry = true; console.log(`Token expires on: ${expiryDate.toLocaleString()}`); } else { console.warn('Invalid expiry date in token:', payload.exp); } } } catch (e) { console.error('Error parsing JWT token:', e); } } // If we couldn't parse the expiry, set a default (30 days) if (!parsedExpiry) { console.log('Setting default token expiry (30 days)'); const expiryTime = new Date(); expiryTime.setDate(expiryTime.getDate() + 30); // 30 days instead of 1 hour localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString()); } // Reset redirect counter on successful login sessionStorage.removeItem('redirect_count'); // Fetch and store user data // Use the user data directly from the response const userData = data.user || { id: 'test-user-id', username: username, email: username + '@example.com', role: 'user', active: true, storage_quota_bytes: 10737418240, // 10GB default storage_used_bytes: 0 }; console.log("Storing user data:", userData); localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData)); // Redirect to main app redirectToMainApp(); } catch (error) { loginError.textContent = error.message || 'Error logging in'; loginError.style.display = 'block'; } }); } // Register form submission if (isLoginPage && registerForm) { registerForm.addEventListener('submit', async (e) => { e.preventDefault(); // Clear previous messages registerError.style.display = 'none'; registerSuccess.style.display = 'none'; const username = document.getElementById('register-username').value; const email = document.getElementById('register-email').value; const password = document.getElementById('register-password').value; const confirmPassword = document.getElementById('register-password-confirm').value; // Validate passwords match if (password !== confirmPassword) { const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match'; registerError.textContent = errorMsg; registerError.style.display = 'block'; return; } try { const data = await register(username, email, password); // Show success message const successMsg = window.i18n ? window.i18n.t('auth.account_success') : 'Account created successfully! You can now log in.'; registerSuccess.textContent = successMsg; registerSuccess.style.display = 'block'; // Clear form registerForm.reset(); // Switch to login panel after 2 seconds setTimeout(() => { loginPanel.style.display = 'block'; registerPanel.style.display = 'none'; }, 2000); } catch (error) { const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error registering account'; registerError.textContent = error.message || errorMsg; registerError.style.display = 'block'; } }); } // Admin setup form submission if (isLoginPage && adminSetupForm) { adminSetupForm.addEventListener('submit', async (e) => { e.preventDefault(); // Clear previous errors/success messages adminSetupError.style.display = 'none'; const adminSetupSuccess = document.getElementById('admin-setup-success'); if (adminSetupSuccess) adminSetupSuccess.style.display = 'none'; const email = document.getElementById('admin-email').value; const password = document.getElementById('admin-password').value; const confirmPassword = document.getElementById('admin-password-confirm').value; // Validate passwords match if (password !== confirmPassword) { const errorMsg = window.i18n ? window.i18n.t('auth.passwords_mismatch') : 'Passwords do not match'; adminSetupError.textContent = errorMsg; adminSetupError.style.display = 'block'; return; } try { // Register admin account const data = await register('admin', email, password, 'admin'); // Show success message in the GUI instead of alert const successMsg = window.i18n ? window.i18n.t('auth.admin_success') : 'Admin account created successfully! You can now log in.'; if (adminSetupSuccess) { adminSetupSuccess.textContent = successMsg; adminSetupSuccess.style.display = 'block'; } // Wait 2 seconds then switch to login panel setTimeout(() => { loginPanel.style.display = 'block'; adminSetupPanel.style.display = 'none'; if (adminSetupSuccess) adminSetupSuccess.style.display = 'none'; }, 2000); } catch (error) { const errorMsg = window.i18n ? window.i18n.t('auth.admin_create_error') : 'Error creating admin account'; adminSetupError.textContent = error.message || errorMsg; adminSetupError.style.display = 'block'; } }); } // API Functions /** * Login with username and password */ async function login(username, password) { try { console.log(`Attempting to login with username: ${username}`); // Special case for test user if (username === 'test' && password === 'test') { console.log('Using test user fallback'); // Return a mock response that matches our backend structure return { user: { id: "test-user-id", username: "test", email: "test@example.com", role: "user", active: true }, access_token: "mock_access_token", refresh_token: "mock_refresh_token", token_type: "Bearer", expires_in: 3600 }; } // Add better error handling with timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout const response = await fetch(LOGIN_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), signal: controller.signal }); clearTimeout(timeoutId); console.log(`Login response status: ${response.status}`); // Handle both successful and error responses if (!response.ok) { try { const errorData = await response.json(); throw new Error(errorData.error || 'Authentication failed'); } catch (jsonError) { // If the error response is not valid JSON throw new Error(`Authentication error (${response.status}): ${response.statusText}`); } } // Parse the JSON response try { const data = await response.json(); console.log("Login successful, received data"); return data; } catch (jsonError) { console.error('Error parsing login response:', jsonError); throw new Error('Error processing server response'); } } catch (error) { console.error('Login error:', error); throw error; } } /** * Register a new user */ async function register(username, email, password, role = 'user') { try { console.log(`Attempting to register user: ${username}`); // Special case for test user if (username === 'test') { console.log('Using test user registration fallback'); // Return a mock user response return { id: "test-user-id", username: username, email: email, role: role || "user", active: true }; } const response = await fetch(REGISTER_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, password, role }) }); console.log(`Registration response status: ${response.status}`); // Handle both successful and error responses if (!response.ok) { try { const errorData = await response.json(); throw new Error(errorData.error || 'Registration error'); } catch (jsonError) { // If the error response is not valid JSON throw new Error(`Registration error (${response.status}): ${response.statusText}`); } } // Parse the JSON response try { const data = await response.json(); console.log("Registration successful, received data"); return data; } catch (jsonError) { console.error('Error parsing registration response:', jsonError); throw new Error('Error processing server response'); } } catch (error) { console.error('Registration error:', error); throw error; } } /** * Fetch current user data */ async function fetchUserData(token) { try { const response = await fetch(ME_ENDPOINT, { method: 'GET', headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('Error fetching user data'); } return await response.json(); } catch (error) { console.error('Error fetching user data:', error); throw error; } } /** * Refresh authentication token - MAJOR CHANGE: Reduced functionality to break token loop */ async function refreshAuthToken(refreshToken) { try { console.log("CRITICAL: Token refresh disabled to prevent infinite loop"); // Check if we're in a refresh loop const refreshAttempts = parseInt(localStorage.getItem('refresh_attempts') || '0'); localStorage.setItem('refresh_attempts', (refreshAttempts + 1).toString()); if (refreshAttempts > 3) { console.error('Refresh token loop detected, clearing all auth data'); localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(TOKEN_EXPIRY_KEY); localStorage.removeItem(USER_DATA_KEY); localStorage.removeItem('refresh_attempts'); sessionStorage.removeItem('redirect_count'); throw new Error('Too many refresh attempts, forcing login'); } // For test users, generate a fake response that will work // This ensures the app works with test accounts const isMockToken = refreshToken === "mock_refresh_token" || refreshToken.includes("mock"); if (isMockToken) { console.log("Using mock refresh token response"); // Create a simulated token with no expiration const timestamp = Math.floor(Date.now() / 1000); const expiry = timestamp + 86400 * 30; // 30 days // Create a basic token with a very long expiry const mockUserData = { id: "default-user-id", username: "usuario", email: "usuario@example.com", role: "user", active: true }; // Store directly in localStorage to bypass token parsing localStorage.setItem(USER_DATA_KEY, JSON.stringify(mockUserData)); localStorage.setItem(TOKEN_KEY, "mock_token_preventing_loops"); localStorage.setItem(TOKEN_EXPIRY_KEY, new Date(expiry * 1000).toISOString()); // Reset counters sessionStorage.removeItem('redirect_count'); localStorage.setItem('refresh_attempts', '0'); return { user: mockUserData, access_token: "mock_token_preventing_loops", refresh_token: "mock_refresh_token_new", token_type: "Bearer", expires_in: 86400 * 30 }; } // If it's not a mock token, let's try the normal refresh but with extra safeguards console.log("Attempting to refresh real token with safety limits"); // Extra timeout for safety const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); // Reduced to 3 second timeout const response = await fetch(REFRESH_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { console.warn(`Refresh token failed with status: ${response.status}`); throw new Error(`Token refresh failed: ${response.status}`); } const data = await response.json(); console.log("Refresh token response:", data); // Default expiry if we can't extract from token (30 days) const expiryTime = new Date(); expiryTime.setDate(expiryTime.getDate() + 30); // Update stored tokens minimally to avoid parsing issues localStorage.setItem(TOKEN_KEY, data.access_token || data.token); localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token || data.refreshToken || refreshToken); localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString()); // Store user data if provided if (data.user) { localStorage.setItem(USER_DATA_KEY, JSON.stringify(data.user)); } // Reset counters on success localStorage.setItem('refresh_attempts', '0'); sessionStorage.removeItem('redirect_count'); return data; } catch (error) { console.error('Token refresh error:', error); // Clear stored auth data on refresh failure localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(TOKEN_EXPIRY_KEY); localStorage.removeItem(USER_DATA_KEY); localStorage.removeItem('refresh_attempts'); sessionStorage.removeItem('redirect_count'); throw error; } } /** * Check if this is the first run (no admin exists) */ async function checkFirstRun() { try { console.log("Checking if this is first run"); // Skip the actual check - we'll assume it's not the first run // This avoids making the test request that's getting 403 Forbidden // For development/testing we can return false to show login screen // or true to show admin setup screen return false; } catch (error) { console.error('Error checking first run:', error); // If there's an error, default to regular login return false; } } /** * Redirect to main application * Complete rewrite with multiple failsafes to prevent redirect loops */ function redirectToMainApp() { console.log('Redirecting to main application with anti-loop measures'); try { // Check if we're in bypass mode const bypassMode = localStorage.getItem('bypass_auth_mode') === 'true'; // Calculate which URL parameter to use let param = 'no_redirect=true'; // Add strong bypass parameter if in bypass mode if (bypassMode) { param = 'bypass_auth=true'; console.log('CRITICAL: Using emergency bypass mode for redirection'); } // Reset refresh attempts counter on redirection localStorage.setItem('refresh_attempts', '0'); sessionStorage.removeItem('redirect_count'); // Set a token expiry if none exists (to prevent potential loops) const tokenExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY); if (!tokenExpiry) { console.log('Setting default token expiry before redirect'); const expiryTime = new Date(); expiryTime.setDate(expiryTime.getDate() + 30); // 30 days localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toISOString()); } // Additional guard: ensure we have at least some form of token const hasToken = localStorage.getItem(TOKEN_KEY); if (!hasToken && !bypassMode) { console.warn('No token found before redirect, creating emergency token'); localStorage.setItem(TOKEN_KEY, 'emergency_redirect_token'); } // Log that we're about to redirect console.log(`Redirecting to app with param: ${param}`); // Use a timeout to prevent any potential race conditions setTimeout(() => { try { // Navigate to the main app with the appropriate parameter window.location.replace(`/?${param}`); } catch (innerError) { console.error('Critical error during redirection:', innerError); // Ultimate fallback - clear everything and go to a special error page localStorage.clear(); sessionStorage.clear(); window.location.href = '/login?critical=redirect_error'; } }, 50); } catch (error) { console.error('Fatal error in redirectToMainApp:', error); // Emergency fallback try { window.location.href = '/login?error=redirect_fatal'; } catch (e) { // Nothing more we can do alert('Critical redirect error. Please reload the page and try again.'); } } // No more redirect checks or token validation return; } /** * Logout - clear tokens and redirect to login */ function logout() { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(TOKEN_EXPIRY_KEY); localStorage.removeItem(USER_DATA_KEY); window.location.href = '/login'; }