/** * User menu, profile modal and logout logic */ import { getCsrfHeaders } from '../core/csrf.js'; import { formatFileSize, formatQuotaSize } from '../core/formatters.js'; import { i18n } from '../core/i18n.js'; import { ui } from './ui.js'; function setupUserMenu() { const wrapper = document.getElementById('user-menu-wrapper'); const avatarBtn = document.getElementById('user-avatar-btn'); const menu = document.getElementById('user-menu'); const logoutBtn = document.getElementById('user-menu-logout'); const themeBtn = document.getElementById('user-menu-theme'); const aboutBtn = document.getElementById('user-menu-about'); const adminBtn = document.getElementById('user-menu-admin'); const adminDivider = document.getElementById('user-menu-admin-divider'); const profileBtn = document.getElementById('user-menu-profile'); const roleBadge = document.getElementById('user-menu-role-badge'); if (!wrapper || !avatarBtn || !menu) return; avatarBtn.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = wrapper.classList.contains('open'); wrapper.classList.toggle('open'); const notifWrapper = document.getElementById('notif-wrapper'); const notifBtn = document.getElementById('notif-bell-btn'); if (notifWrapper) notifWrapper.classList.remove('open'); if (notifBtn) notifBtn.classList.remove('active'); if (!isOpen) { updateUserMenuData(); const USER_DATA_KEY = 'oxicloud_user'; const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); const isAdmin = userData.role === 'admin'; if (adminBtn) { isAdmin ? adminBtn.classList.remove('hidden') : adminBtn.classList.add('hidden'); } if (adminDivider) { isAdmin ? adminDivider.classList.remove('hidden') : adminDivider.classList.add('hidden'); } if (roleBadge) { isAdmin ? roleBadge.classList.remove('hidden') : roleBadge.classList.add('hidden'); } } }); document.addEventListener('click', (e) => { if (wrapper.classList.contains('open') && !wrapper.contains(e.target)) { wrapper.classList.remove('open'); } }); if (logoutBtn) { logoutBtn.addEventListener('click', () => { wrapper.classList.remove('open'); logout(); }); } if (themeBtn) { const pill = document.getElementById('theme-toggle-pill'); // Sync pill UI with current theme state function syncThemePill() { const isDark = localStorage.getItem('oxicloud_theme') === 'dark'; if (pill) { if (isDark) { pill.classList.add('active'); } else { pill.classList.remove('active'); } } // Ensure document theme matches localStorage if (isDark) { document.documentElement.setAttribute('data-theme', 'dark'); } else { document.documentElement.removeAttribute('data-theme'); } } // Initialize pill state on load syncThemePill(); themeBtn.addEventListener('click', (e) => { e.stopPropagation(); // Toggle theme based on current state, not pill state const currentIsDark = localStorage.getItem('oxicloud_theme') === 'dark'; const newIsDark = !currentIsDark; localStorage.setItem('oxicloud_theme', newIsDark ? 'dark' : 'light'); if (newIsDark) { document.documentElement.setAttribute('data-theme', 'dark'); } else { document.documentElement.removeAttribute('data-theme'); } if (pill) { if (newIsDark) { pill.classList.add('active'); } else { pill.classList.remove('active'); } } ui.showNotification(newIsDark ? '🌙' : '☀️', newIsDark ? 'Dark mode enabled' : 'Light mode enabled'); }); } if (adminBtn) { adminBtn.addEventListener('click', () => { wrapper.classList.remove('open'); window.location.href = '/admin'; }); } if (profileBtn) { profileBtn.addEventListener('click', () => { wrapper.classList.remove('open'); window.location.href = '/profile'; }); } if (aboutBtn) { aboutBtn.addEventListener('click', () => { wrapper.classList.remove('open'); const overlay = document.getElementById('about-modal-overlay'); if (overlay) overlay.classList.remove('hidden'); }); } const aboutCloseBtn = document.getElementById('about-close-btn'); const aboutOverlay = document.getElementById('about-modal-overlay'); if (aboutCloseBtn) { aboutCloseBtn.addEventListener('click', () => { aboutOverlay.classList.add('hidden'); }); } if (aboutOverlay) { aboutOverlay.addEventListener('click', (e) => { if (e.target === aboutOverlay) { aboutOverlay.classList.add('hidden'); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !aboutOverlay.classList.contains('hidden')) { aboutOverlay.classList.add('hidden'); } }); } fetchAppVersion(); } function updateUserMenuData() { const USER_DATA_KEY = 'oxicloud_user'; const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); const nameEl = document.getElementById('user-menu-name'); const emailEl = document.getElementById('user-menu-email'); const avatarEl = document.getElementById('user-menu-avatar'); const storageFill = document.getElementById('user-menu-storage-fill'); const storageText = document.getElementById('user-menu-storage-text'); if (userData.username) { if (nameEl) nameEl.textContent = userData.username; if (emailEl) emailEl.textContent = userData.email || ''; if (avatarEl) avatarEl.textContent = userData.username.substring(0, 2).toUpperCase(); } const usedBytes = userData.storage_used_bytes || 0; const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes; const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0; if (storageFill) storageFill.style.width = `${percentage}%`; if (storageText) { const used = formatFileSize(usedBytes); const total = formatQuotaSize(quotaBytes); storageText.textContent = `${quotaBytes > 0 ? `${percentage}% · ` : ''}${used} / ${total}`; } } async function fetchAppVersion() { try { const response = await fetch('/api/version'); if (response.ok) { const data = await response.json(); const versionEl = document.getElementById('about-version'); if (versionEl && data.version) { versionEl.textContent = `v${data.version}`; } } } catch (err) { console.warn('Could not fetch app version:', err); } } function showUserProfileModal() { const USER_DATA_KEY = 'oxicloud_user'; const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); const username = userData.username || 'User'; const email = userData.email || ''; const role = userData.role || 'user'; const initials = username.substring(0, 2).toUpperCase(); const usedBytes = userData.storage_used_bytes || 0; const quotaBytes = userData.storage_quota_bytes == null ? 10 * 1024 * 1024 * 1024 : userData.storage_quota_bytes; const percentage = quotaBytes > 0 ? Math.min(Math.round((usedBytes / quotaBytes) * 100), 100) : 0; // FIXME: use classes const barColor = percentage > 90 ? '#ef4444' : percentage > 70 ? '#f59e0b' : '#22c55e'; const t = (key, fallback) => i18n.t(key) || fallback; const existing = document.getElementById('profile-modal-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'profile-modal-overlay'; overlay.classList.add('about-modal-overlay', 'hidden'); overlay.innerHTML = `
${initials}

${username}

${role === 'admin' ? '🛡️ Admin' : `👤 ${t('user_menu.role_user', 'User')}`}
${t('storage.title', 'Storage')}
${percentage}% · ${formatFileSize(usedBytes)} / ${formatQuotaSize(quotaBytes)}
`; // Set dynamic bar width and color via JS property (CSP-safe) const barFill = overlay.querySelector('#about-bar-fill'); if (barFill) { barFill.style.width = `${percentage}%`; barFill.style.background = barColor; } document.body.appendChild(overlay); requestAnimationFrame(() => overlay.classList.add('show')); overlay.querySelector('#profile-modal-close').addEventListener('click', () => { overlay.classList.remove('show'); setTimeout(() => overlay.remove(), 200); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.classList.remove('show'); setTimeout(() => overlay.remove(), 200); } }); } async function logout() { const USER_DATA_KEY = 'oxicloud_user'; // Clear local state first to prevent login page from auto-refreshing localStorage.removeItem(USER_DATA_KEY); localStorage.removeItem('refresh_attempts'); sessionStorage.removeItem('redirect_count'); // Tell the server to clear HttpOnly cookies (await to ensure cookies are // cleared before redirecting, otherwise the login page's session probe // will refresh the token and redirect back to the app). try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin', headers: getCsrfHeaders() }); } catch (_) { // Best-effort } window.location.href = '/login'; } export { logout, setupUserMenu, showUserProfileModal };