f25987e553
Root cause: localStorage keys 'oxicloud_recent_files' and
'oxicloud_favorites' were global — shared across all users on the same
browser. When user A logged out and user B logged in, user B could see
(and access) user A's recent files and favorites.
Fixes applied:
recent.js:
- Storage key now user-specific: 'oxicloud_recent_files_{username}'
- getStorageKey() derives key from current user in localStorage
- migrateFromLegacyKey() moves data from old global key on init
- Legacy global key is always removed after migration
favorites.js:
- Same pattern: 'oxicloud_favorites_{username}'
- getStorageKey() + migrateFromLegacyKey() added
auth.js (logout):
- Clears user-specific recent and favorites keys before removing
user data, plus removes any legacy global keys
Bumps service worker cache to v13.
1233 lines
49 KiB
JavaScript
1233 lines
49 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<span class="lang-picker-item-flag">${lang.flag}</span>
|
|
<span class="lang-picker-item-name">${lang.nativeName}</span>
|
|
<span class="lang-picker-item-english">${lang.name}</span>
|
|
${isSelected ? '<i class="fas fa-check lang-picker-item-check"></i>' : ''}
|
|
`;
|
|
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 = '<div class="lang-picker-empty">—</div>';
|
|
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() {
|
|
// Clear user-specific recent files and favorites before removing user data
|
|
try {
|
|
const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}');
|
|
if (userData.username) {
|
|
localStorage.removeItem(`oxicloud_recent_files_${userData.username}`);
|
|
localStorage.removeItem(`oxicloud_favorites_${userData.username}`);
|
|
}
|
|
} catch (e) {
|
|
// Ignore parse errors during cleanup
|
|
}
|
|
// Also remove any legacy global keys
|
|
localStorage.removeItem('oxicloud_recent_files');
|
|
localStorage.removeItem('oxicloud_favorites');
|
|
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
|
localStorage.removeItem(TOKEN_EXPIRY_KEY);
|
|
localStorage.removeItem(USER_DATA_KEY);
|
|
window.location.href = '/login';
|
|
} |