
- Implement a unified SPA routing system in nav.js, removing all legacy and conflicting navigation scripts (router.js, inject-nav.js, fix-nav.js). - Refactor dashboard.js to delegate all navigation handling to the new nav.js module. - Create new modular JS files (auth.js, personal-player.js, logger.js) to improve code organization. - Fix all navigation-related bugs, including guest access and broken footer links. - Clean up the project root by moving development scripts and backups to a dedicated /dev directory. - Add a .gitignore file to exclude the database, logs, and other transient files from the repository.
253 lines
8.9 KiB
JavaScript
253 lines
8.9 KiB
JavaScript
import { showToast } from './toast.js';
|
|
|
|
import { loadProfileStream } from './personal-player.js';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Track previous authentication state
|
|
let wasAuthenticated = null;
|
|
// Debug flag - set to false to disable auth state change logs
|
|
const DEBUG_AUTH_STATE = false;
|
|
|
|
// Track auth check calls and cache state
|
|
let lastAuthCheckTime = 0;
|
|
let authCheckCounter = 0;
|
|
const AUTH_CHECK_DEBOUNCE = 1000; // 1 second
|
|
let authStateCache = {
|
|
timestamp: 0,
|
|
value: null,
|
|
ttl: 5000 // Cache TTL in milliseconds
|
|
};
|
|
|
|
// Handle magic link login redirect
|
|
function handleMagicLoginRedirect() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('login') === 'success' && params.get('confirmed_uid')) {
|
|
const username = params.get('confirmed_uid');
|
|
console.log('Magic link login detected for user:', username);
|
|
|
|
// Update authentication state
|
|
localStorage.setItem('uid', username);
|
|
localStorage.setItem('confirmed_uid', username);
|
|
localStorage.setItem('uid_time', Date.now().toString());
|
|
document.cookie = `uid=${encodeURIComponent(username)}; path=/; SameSite=Lax`;
|
|
|
|
// Update UI state
|
|
document.body.classList.add('authenticated');
|
|
document.body.classList.remove('guest');
|
|
|
|
// Update local storage and cookies
|
|
localStorage.setItem('isAuthenticated', 'true');
|
|
document.cookie = `isAuthenticated=true; path=/; SameSite=Lax`;
|
|
|
|
// Update URL and history without reloading
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
// Update navigation
|
|
if (typeof injectNavigation === 'function') {
|
|
console.log('Updating navigation after magic link login');
|
|
injectNavigation(true);
|
|
} else {
|
|
console.warn('injectNavigation function not available after magic link login');
|
|
}
|
|
|
|
// Navigate to user's profile page
|
|
if (window.showOnly) {
|
|
console.log('Navigating to me-page');
|
|
window.showOnly('me-page');
|
|
} else if (window.location.hash !== '#me') {
|
|
window.location.hash = '#me';
|
|
}
|
|
|
|
// Auth state will be updated by the polling mechanism
|
|
}
|
|
}
|
|
|
|
// Update the visibility of the account deletion section based on authentication state
|
|
function updateAccountDeletionVisibility(isAuthenticated) {
|
|
const authOnlyWrapper = document.querySelector('#privacy-page .auth-only');
|
|
const accountDeletionSection = document.getElementById('account-deletion');
|
|
|
|
const showElement = (element) => {
|
|
if (!element) return;
|
|
element.classList.remove('hidden', 'auth-only-hidden');
|
|
element.style.display = 'block';
|
|
};
|
|
|
|
const hideElement = (element) => {
|
|
if (!element) return;
|
|
element.style.display = 'none';
|
|
};
|
|
|
|
if (isAuthenticated) {
|
|
const isPrivacyPage = window.location.hash === '#privacy-page';
|
|
if (isPrivacyPage) {
|
|
if (authOnlyWrapper) showElement(authOnlyWrapper);
|
|
if (accountDeletionSection) showElement(accountDeletionSection);
|
|
} else {
|
|
if (accountDeletionSection) hideElement(accountDeletionSection);
|
|
if (authOnlyWrapper) hideElement(authOnlyWrapper);
|
|
}
|
|
} else {
|
|
if (accountDeletionSection) hideElement(accountDeletionSection);
|
|
if (authOnlyWrapper) {
|
|
const hasOtherContent = Array.from(authOnlyWrapper.children).some(
|
|
child => child.id !== 'account-deletion' && child.offsetParent !== null
|
|
);
|
|
if (!hasOtherContent) {
|
|
hideElement(authOnlyWrapper);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check authentication state and update UI with caching and debouncing
|
|
function checkAuthState(force = false) {
|
|
const now = Date.now();
|
|
if (!force && authStateCache.value !== null && now - authStateCache.timestamp < authStateCache.ttl) {
|
|
return authStateCache.value;
|
|
}
|
|
|
|
if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE && !force) {
|
|
return wasAuthenticated;
|
|
}
|
|
lastAuthCheckTime = now;
|
|
authCheckCounter++;
|
|
|
|
const isAuthenticated =
|
|
(document.cookie.includes('isAuthenticated=true') || localStorage.getItem('isAuthenticated') === 'true') &&
|
|
(document.cookie.includes('uid=') || localStorage.getItem('uid')) &&
|
|
!!localStorage.getItem('authToken');
|
|
|
|
authStateCache = {
|
|
timestamp: now,
|
|
value: isAuthenticated,
|
|
ttl: isAuthenticated ? 30000 : 5000
|
|
};
|
|
|
|
if (isAuthenticated !== wasAuthenticated) {
|
|
if (DEBUG_AUTH_STATE) {
|
|
console.log('Auth state changed, updating UI...');
|
|
}
|
|
|
|
if (!isAuthenticated && wasAuthenticated) {
|
|
console.log('User was authenticated, but is no longer. Triggering logout.');
|
|
basicLogout();
|
|
return; // Stop further processing after logout
|
|
}
|
|
|
|
if (isAuthenticated) {
|
|
document.body.classList.add('authenticated');
|
|
document.body.classList.remove('guest');
|
|
const uid = localStorage.getItem('uid');
|
|
if (uid && (window.location.hash === '#me-page' || window.location.hash === '#me' || window.location.pathname.startsWith('/~'))) {
|
|
loadProfileStream(uid);
|
|
}
|
|
} else {
|
|
document.body.classList.remove('authenticated');
|
|
document.body.classList.add('guest');
|
|
}
|
|
|
|
updateAccountDeletionVisibility(isAuthenticated);
|
|
wasAuthenticated = isAuthenticated;
|
|
void document.body.offsetHeight; // Force reflow
|
|
}
|
|
|
|
return isAuthenticated;
|
|
}
|
|
|
|
// Periodically check authentication state with optimized polling
|
|
function setupAuthStatePolling() {
|
|
checkAuthState(true);
|
|
|
|
const checkAndUpdate = () => {
|
|
checkAuthState(!document.hidden);
|
|
};
|
|
|
|
const AUTH_CHECK_INTERVAL = 30000;
|
|
setInterval(checkAndUpdate, AUTH_CHECK_INTERVAL);
|
|
|
|
const handleStorageEvent = (e) => {
|
|
if (['isAuthenticated', 'authToken', 'uid'].includes(e.key)) {
|
|
checkAuthState(true);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('storage', handleStorageEvent);
|
|
|
|
const handleVisibilityChange = () => {
|
|
if (!document.hidden) {
|
|
checkAuthState(true);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
return () => {
|
|
window.removeEventListener('storage', handleStorageEvent);
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
};
|
|
}
|
|
|
|
// --- ACCOUNT DELETION ---
|
|
const deleteAccount = async (e) => {
|
|
if (e) e.preventDefault();
|
|
if (deleteAccount.inProgress) return;
|
|
if (!confirm('Are you sure you want to delete your account?\nThis action is permanent.')) return;
|
|
|
|
deleteAccount.inProgress = true;
|
|
const deleteBtn = e?.target.closest('button');
|
|
const originalText = deleteBtn?.textContent;
|
|
if (deleteBtn) {
|
|
deleteBtn.disabled = true;
|
|
deleteBtn.textContent = 'Deleting...';
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/delete-account', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ uid: localStorage.getItem('uid') })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ detail: 'Failed to delete account.' }));
|
|
throw new Error(errorData.detail);
|
|
}
|
|
|
|
showToast('Account deleted successfully.', 'success');
|
|
// Perform a full client-side logout and redirect
|
|
basicLogout();
|
|
} catch (error) {
|
|
showToast(error.message, 'error');
|
|
} finally {
|
|
deleteAccount.inProgress = false;
|
|
if (deleteBtn) {
|
|
deleteBtn.disabled = false;
|
|
deleteBtn.textContent = originalText;
|
|
}
|
|
}
|
|
};
|
|
|
|
// --- LOGOUT ---
|
|
function basicLogout() {
|
|
['isAuthenticated', 'uid', 'confirmed_uid', 'uid_time', 'authToken'].forEach(k => localStorage.removeItem(k));
|
|
document.cookie.split(';').forEach(c => document.cookie = c.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`));
|
|
window.location.href = '/';
|
|
}
|
|
|
|
// --- DELEGATED EVENT LISTENERS ---
|
|
document.addEventListener('click', (e) => {
|
|
|
|
// Delete Account Buttons
|
|
if (e.target.closest('#delete-account') || e.target.closest('#delete-account-from-privacy')) {
|
|
deleteAccount(e);
|
|
return;
|
|
}
|
|
});
|
|
|
|
// --- INITIALIZATION ---
|
|
handleMagicLoginRedirect();
|
|
setupAuthStatePolling();
|
|
});
|