From da28b205e52a375adb390a058e96030f09e259e1 Mon Sep 17 00:00:00 2001 From: oib Date: Sun, 20 Jul 2025 09:24:51 +0200 Subject: [PATCH] fix: resolve mobile navigation visibility for authenticated users - Add fix-nav.js to handle navigation state - Update mobile.css with more specific selectors - Modify dashboard.js to ensure proper auth state - Update index.html to include the new fix script - Ensure guest navigation stays hidden during client-side navigation --- static/dashboard.js | 386 +++++++++++++++++++++++++++++++------------- static/fix-nav.js | 134 +++++++++++++++ static/index.html | 45 +++--- static/mobile.css | 46 +++++- 4 files changed, 479 insertions(+), 132 deletions(-) create mode 100644 static/fix-nav.js diff --git a/static/dashboard.js b/static/dashboard.js index 2c75e7e..1c931fb 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -13,8 +13,12 @@ function getCookie(name) { let isLoggingOut = false; async function handleLogout(event) { + console.log('[LOGOUT] Logout initiated'); // Prevent multiple simultaneous logout attempts - if (isLoggingOut) return; + if (isLoggingOut) { + console.log('[LOGOUT] Logout already in progress'); + return; + } isLoggingOut = true; // Prevent default button behavior @@ -23,44 +27,145 @@ async function handleLogout(event) { event.stopPropagation(); } + // Get auth token before we clear it + const authToken = localStorage.getItem('authToken'); + + // 1. First try to invalidate the server session (but don't block on it) + if (authToken) { + try { + // We'll use a timeout to prevent hanging on the server request + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 2000); + + try { + await fetch('/api/logout', { + method: 'POST', + credentials: 'include', + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + }); + clearTimeout(timeoutId); + } catch (error) { + clearTimeout(timeoutId); + // Silently handle any errors during server logout + } + } catch (error) { + // Silently handle any unexpected errors + } + } + + // 2. Clear all client-side state + function clearClientState() { + console.log('[LOGOUT] Clearing client state'); + + // Clear all authentication-related data from localStorage + const keysToRemove = [ + 'uid', 'uid_time', 'confirmed_uid', 'last_page', + 'isAuthenticated', 'authToken', 'user', 'token', 'sessionid' + ]; + + keysToRemove.forEach(key => { + localStorage.removeItem(key); + sessionStorage.removeItem(key); + }); + + // Get current cookies for debugging + const cookies = document.cookie.split(';'); + console.log('[LOGOUT] Current cookies before clearing:', cookies); + + // Function to clear a cookie by name + const clearCookie = (name) => { + console.log(`[LOGOUT] Attempting to clear cookie: ${name}`); + const baseOptions = 'Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=Lax'; + // Try with current domain + document.cookie = `${name}=; ${baseOptions}`; + // Try with domain + document.cookie = `${name}=; ${baseOptions}; domain=${window.location.hostname}`; + // Try with leading dot for subdomains + document.cookie = `${name}=; ${baseOptions}; domain=.${window.location.hostname}`; + }; + + // Clear all authentication-related cookies + const authCookies = [ + 'uid', 'authToken', 'isAuthenticated', 'sessionid', 'session_id', + 'token', 'remember_token', 'auth', 'authentication' + ]; + + // Clear specific auth cookies + authCookies.forEach(clearCookie); + + // Also clear any existing cookies that match our patterns + cookies.forEach(cookie => { + const [name] = cookie.trim().split('='); + if (name && authCookies.some(authName => name.trim() === authName)) { + clearCookie(name.trim()); + } + }); + + // Clear all cookies by setting them to expire in the past + document.cookie.split(';').forEach(cookie => { + const [name] = cookie.trim().split('='); + if (name) { + clearCookie(name.trim()); + } + }); + + console.log('[LOGOUT] Cookies after clearing:', document.cookie); + + // Update UI state + document.body.classList.remove('authenticated'); + document.body.classList.add('guest'); + + // Force a hard reload to ensure all state is reset + setTimeout(() => { + // Clear all storage again before redirecting + keysToRemove.forEach(key => { + localStorage.removeItem(key); + sessionStorage.removeItem(key); + }); + + // Redirect to home with a cache-busting parameter + window.location.href = '/?logout=' + Date.now(); + }, 100); + } + try { - console.log('[LOGOUT] Starting logout process'); + // Clear client state immediately to prevent any race conditions + clearClientState(); - // Clear user data from localStorage - localStorage.removeItem('uid'); - localStorage.removeItem('uid_time'); - localStorage.removeItem('confirmed_uid'); - localStorage.removeItem('last_page'); - - // Clear session cookie with SameSite attribute to match how it was set - const isLocalhost = window.location.hostname === 'localhost'; - const secureFlag = !isLocalhost ? '; Secure' : ''; - document.cookie = `sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=Lax${secureFlag};`; - - // Update UI state immediately - const userDashboard = document.getElementById('user-dashboard'); - const guestDashboard = document.getElementById('guest-dashboard'); - const logoutButton = document.getElementById('logout-button'); - const deleteAccountButton = document.getElementById('delete-account-button'); - - if (userDashboard) userDashboard.style.display = 'none'; - if (guestDashboard) guestDashboard.style.display = 'block'; - if (logoutButton) logoutButton.style.display = 'none'; - if (deleteAccountButton) deleteAccountButton.style.display = 'none'; - - // Show success message (only once) - if (window.showToast) { - showToast('Logged out successfully'); - } else { - console.log('Logged out successfully'); + // 2. Try to invalidate the server session (but don't block on it) + console.log('[LOGOUT] Auth token exists:', !!authToken); + if (authToken) { + try { + console.log('[LOGOUT] Attempting to invalidate server session'); + + const response = await fetch('/api/logout', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + }); + + if (!response.ok && response.status !== 401) { + console.warn(`[LOGOUT] Server returned ${response.status} during logout`); + // Don't throw - we've already cleared client state + } else { + console.log('[LOGOUT] Server session invalidated successfully'); + } + } catch (error) { + console.warn('[LOGOUT] Error during server session invalidation (non-critical):', error); + // Continue with logout process + } } - // Navigate to register page - if (window.showOnly) { - window.showOnly('register-page'); - } else { - // Fallback to URL change if showOnly isn't available - window.location.href = '/#register-page'; + // 3. Update navigation if the function exists + if (typeof injectNavigation === 'function') { + injectNavigation(false); } console.log('[LOGOUT] Logout completed'); @@ -72,6 +177,11 @@ async function handleLogout(event) { } } finally { isLoggingOut = false; + + // 4. Redirect to home page after a short delay to ensure state is cleared + setTimeout(() => { + window.location.href = '/'; + }, 100); } } @@ -194,6 +304,15 @@ async function initDashboard() { const hasAuthToken = localStorage.getItem('authToken') !== null; const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken; + // Ensure body class reflects authentication state + if (isAuthenticated) { + document.body.classList.add('authenticated'); + document.body.classList.remove('guest-mode'); + } else { + document.body.classList.remove('authenticated'); + document.body.classList.add('guest-mode'); + } + // Debug authentication state console.log('[AUTH] Authentication state:', { hasAuthCookie, @@ -485,52 +604,16 @@ async function initDashboard() { } } -// Function to delete a file - only define if not already defined -if (typeof window.deleteFile === 'undefined') { - window.deleteFile = async function(uid, fileName, listItem) { - if (!confirm(`Are you sure you want to delete "${fileName}"? This cannot be undone.`)) { - return; - } +// Delete file function is defined below with more complete implementation - try { - console.log(`[FILES] Deleting file: ${fileName} for user: ${uid}`); - - const response = await fetch(`/delete-file/${uid}/${encodeURIComponent(fileName)}`, { - method: 'DELETE', - credentials: 'include', - headers: { - 'Accept': 'application/json' - } - }); - - console.log('[FILES] Delete response:', response.status, response.statusText); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[FILES] Delete error:', errorText); - showToast(`Error deleting file: ${response.statusText}`, 'error'); - return; - } - - // Remove the file from the UI - if (listItem && listItem.parentNode) { - listItem.parentNode.removeChild(listItem); - } - - showToast('File deleted successfully', 'success'); - - // If no files left, show "no files" message - const fileList = document.getElementById('file-list'); - if (fileList && fileList.children.length === 0) { - fileList.innerHTML = '
  • No files uploaded yet.
  • '; - } - - } catch (error) { - console.error('[FILES] Error deleting file:', error); - showToast('Error deleting file. Please try again.', 'error'); - } - }; // Close the function expression -} // Close the if statement +// Helper function to format file size +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} // Function to fetch and display user's uploaded files async function fetchAndDisplayFiles(uid) { @@ -560,8 +643,8 @@ async function fetchAndDisplayFiles(uid) { try { // The backend should handle authentication via session cookies // We include the auth token in headers if available, but don't rely on it for auth - console.log('[FILES] Making request to /me with credentials...'); - const response = await fetch('/me', { + console.log(`[FILES] Making request to /me/${uid} with credentials...`); + const response = await fetch(`/me/${uid}`, { method: 'GET', credentials: 'include', // Important: include cookies for session auth headers: headers @@ -617,14 +700,29 @@ async function fetchAndDisplayFiles(uid) { // Clear the loading message fileList.innerHTML = ''; + // Track displayed files to prevent duplicates using stored filenames as unique identifiers + const displayedFiles = new Set(); + // Add each file to the list - files.forEach(fileName => { - const fileExt = fileName.split('.').pop().toLowerCase(); - const fileUrl = `/data/${uid}/${encodeURIComponent(fileName)}`; - const fileSize = 'N/A'; // We don't have size info in the current API response + files.forEach(file => { + // Get the stored filename (with UUID) - this is our unique identifier + const storedFileName = file.stored_name || file.name || file; + + // Skip if we've already displayed this file + if (displayedFiles.has(storedFileName)) { + console.log(`[FILES] Skipping duplicate file with stored name: ${storedFileName}`); + return; + } + + displayedFiles.add(storedFileName); + + const fileExt = storedFileName.split('.').pop().toLowerCase(); + const fileUrl = `/data/${uid}/${encodeURIComponent(storedFileName)}`; + const fileSize = file.size ? formatFileSize(file.size) : 'N/A'; const listItem = document.createElement('li'); listItem.className = 'file-item'; + listItem.setAttribute('data-uid', uid); // Create file icon based on file extension let fileIcon = '📄'; // Default icon @@ -636,11 +734,14 @@ async function fetchAndDisplayFiles(uid) { fileIcon = '📄'; } + // Use original_name if available, otherwise use the stored filename for display + const displayName = file.original_name || storedFileName; + listItem.innerHTML = `
    ${fileIcon} - ${fileName} + ${displayName} ${fileSize}
    @@ -649,18 +750,15 @@ async function fetchAndDisplayFiles(uid) { ⬇️ Download - `; - // Add delete button handler - const deleteButton = listItem.querySelector('.delete-button'); - if (deleteButton) { - deleteButton.addEventListener('click', () => deleteFile(uid, fileName, listItem)); - } + // Delete button handler will be handled by event delegation + // No need to add individual event listeners here fileList.appendChild(listItem); }); @@ -693,31 +791,75 @@ async function fetchAndDisplayFiles(uid) { } // Function to handle file deletion -async function deleteFile(fileId, uid) { - if (!confirm('Are you sure you want to delete this file?')) { +async function deleteFile(uid, fileName, listItem, displayName = '') { + const fileToDelete = displayName || fileName; + if (!confirm(`Are you sure you want to delete "${fileToDelete}"?`)) { return; } + // Show loading state + if (listItem) { + listItem.style.opacity = '0.6'; + listItem.style.pointerEvents = 'none'; + const deleteButton = listItem.querySelector('.delete-file'); + if (deleteButton) { + deleteButton.disabled = true; + deleteButton.innerHTML = 'Deleting...'; + } + } + try { - const response = await fetch(`/api/files/${fileId}`, { + if (!uid) { + throw new Error('User not authenticated. Please log in again.'); + } + + console.log(`[DELETE] Attempting to delete file: ${fileName} for user: ${uid}`); + const authToken = localStorage.getItem('authToken'); + const headers = { 'Content-Type': 'application/json' }; + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + // Use the provided UID in the URL + const response = await fetch(`/uploads/${uid}/${encodeURIComponent(fileName)}`, { method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'same-origin' + headers: headers, + credentials: 'include' }); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); } - // Refresh the file list - fetchAndDisplayFiles(uid); - showToast('File deleted successfully'); + // Remove the file from the UI immediately + if (listItem && listItem.parentNode) { + listItem.parentNode.removeChild(listItem); + } + // Show success message + showToast(`Successfully deleted "${fileToDelete}"`, 'success'); + + // If the file list is now empty, show a message + const fileList = document.getElementById('file-list'); + if (fileList && fileList.children.length === 0) { + fileList.innerHTML = '
  • No files uploaded yet.
  • '; + } } catch (error) { - console.error('[FILES] Error deleting file:', error); - showToast('Error deleting file', 'error'); + console.error('[DELETE] Error deleting file:', error); + showToast(`Error deleting "${fileToDelete}": ${error.message}`, 'error'); + + // Reset the button state if there was an error + if (listItem) { + listItem.style.opacity = ''; + listItem.style.pointerEvents = ''; + const deleteButton = listItem.querySelector('.delete-file'); + if (deleteButton) { + deleteButton.disabled = false; + deleteButton.innerHTML = '🗑️'; + } + } } } @@ -775,7 +917,6 @@ function initFileUpload() { } const result = await response.json(); - showToast('File uploaded successfully!'); // Refresh file list if (window.fetchAndDisplayFiles) { @@ -834,8 +975,33 @@ function initFileUpload() { // Main initialization when the DOM is fully loaded document.addEventListener('DOMContentLoaded', () => { // Initialize dashboard components - initDashboard(); - initFileUpload(); + initDashboard(); // initFileUpload is called from within initDashboard + + // Add event delegation for delete buttons + document.addEventListener('click', (e) => { + const deleteButton = e.target.closest('.delete-file'); + if (!deleteButton) return; + + e.preventDefault(); + e.stopPropagation(); + + const listItem = deleteButton.closest('.file-item'); + if (!listItem) return; + + // Get UID from localStorage + const uid = localStorage.getItem('uid') || localStorage.getItem('confirmed_uid'); + if (!uid) { + showToast('You need to be logged in to delete files', 'error'); + console.error('[DELETE] No UID found in localStorage'); + return; + } + + const fileName = deleteButton.getAttribute('data-filename'); + const displayName = deleteButton.getAttribute('data-original-name') || fileName; + + // Pass the UID to deleteFile + deleteFile(uid, fileName, listItem, displayName); + }); // Make fetchAndDisplayFiles available globally window.fetchAndDisplayFiles = fetchAndDisplayFiles; @@ -874,7 +1040,7 @@ document.addEventListener('DOMContentLoaded', () => { } if (res.ok) { - showToast('', 'success'); + showToast('Check your email for a magic login link!', 'success'); // Clear the form on success regForm.reset(); } else { diff --git a/static/fix-nav.js b/static/fix-nav.js new file mode 100644 index 0000000..3ce5bc4 --- /dev/null +++ b/static/fix-nav.js @@ -0,0 +1,134 @@ +// Force hide guest navigation for authenticated users +function fixMobileNavigation() { + console.log('[FIX-NAV] Running navigation fix...'); + + // Check if user is authenticated + const hasAuthCookie = document.cookie.includes('isAuthenticated=true'); + const hasUidCookie = document.cookie.includes('uid='); + const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true'; + const hasAuthToken = localStorage.getItem('authToken') !== null; + const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken; + + console.log('[FIX-NAV] Authentication state:', { + isAuthenticated, + hasAuthCookie, + hasUidCookie, + hasLocalStorageAuth, + hasAuthToken + }); + + if (isAuthenticated) { + // Force hide guest navigation with !important styles + const guestNav = document.getElementById('guest-dashboard'); + if (guestNav) { + console.log('[FIX-NAV] Hiding guest navigation'); + guestNav.style.cssText = ` + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + height: 0 !important; + width: 0 !important; + padding: 0 !important; + margin: 0 !important; + border: none !important; + position: absolute !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + pointer-events: none !important; + `; + guestNav.classList.add('force-hidden'); + } + + // Ensure user navigation is visible with !important styles + const userNav = document.getElementById('user-dashboard'); + if (userNav) { + console.log('[FIX-NAV] Showing user navigation'); + userNav.style.cssText = ` + display: flex !important; + visibility: visible !important; + opacity: 1 !important; + height: auto !important; + position: relative !important; + clip: auto !important; + pointer-events: auto !important; + `; + userNav.classList.add('force-visible'); + } + + // Add authenticated class to body + document.body.classList.add('authenticated'); + document.body.classList.remove('guest-mode'); + + // Prevent default behavior of nav links that might cause page reloads + document.querySelectorAll('a[href^="#"]').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const targetId = link.getAttribute('href'); + if (targetId && targetId !== '#') { + // Use history API to update URL without full page reload + history.pushState(null, '', targetId); + // Dispatch a custom event that other scripts can listen for + window.dispatchEvent(new CustomEvent('hashchange')); + // Force re-apply our navigation fix + setTimeout(fixMobileNavigation, 0); + } + }); + }); + } else { + // User is not authenticated - ensure guest nav is visible + const guestNav = document.getElementById('guest-dashboard'); + if (guestNav) { + guestNav.style.cssText = ''; // Reset any inline styles + } + document.body.classList.remove('authenticated'); + document.body.classList.add('guest-mode'); + } +} + +// Run on page load +document.addEventListener('DOMContentLoaded', fixMobileNavigation); + +// Also run after a short delay to catch any dynamic content +setTimeout(fixMobileNavigation, 100); +setTimeout(fixMobileNavigation, 300); +setTimeout(fixMobileNavigation, 1000); + +// Listen for hash changes (navigation) +window.addEventListener('hashchange', fixMobileNavigation); + +// Listen for pushState/replaceState (SPA navigation) +const originalPushState = history.pushState; +const originalReplaceState = history.replaceState; + +history.pushState = function() { + originalPushState.apply(this, arguments); + setTimeout(fixMobileNavigation, 0); +}; + +history.replaceState = function() { + originalReplaceState.apply(this, arguments); + setTimeout(fixMobileNavigation, 0); +}; + +// Run on any DOM mutations (for dynamically loaded content) +const observer = new MutationObserver((mutations) => { + let shouldFix = false; + mutations.forEach((mutation) => { + if (mutation.addedNodes.length || mutation.removedNodes.length) { + shouldFix = true; + } + }); + if (shouldFix) { + setTimeout(fixMobileNavigation, 0); + } +}); + +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style', 'id'] +}); + +// Export for debugging +window.fixMobileNavigation = fixMobileNavigation; diff --git a/static/index.html b/static/index.html index 3b4d872..b5dcb84 100644 --- a/static/index.html +++ b/static/index.html @@ -72,6 +72,22 @@
  • Loading files...
  • + + +
    +

    Account Deletion

    +

    This action is irreversible and will permanently remove:

    + + +
    + +
    +
    @@ -81,6 +97,11 @@

    Terms of Service

    +
    + Beta Testing Notice: This service is currently in public beta. As such, you may encounter bugs or unexpected behavior. + Updates to the service may cause data loss. Please report any issues or suggestions to help us improve. +
    +

    By accessing or using dicta2stream.net (the "Service"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree, do not use the Service.

    • You must be at least 18 years old to register.
    • @@ -90,6 +111,8 @@
    • The associated email address will be banned from recreating an account.
    • Uploads are limited to 100 MB and must be voice only.
    • Music/singing will be rejected.
    • +
    • This is a beta service; data may be lost during updates or maintenance.
    • +
    • Please report any bugs or suggestions to help improve the service.
    @@ -103,29 +126,10 @@
  • Users: Session uses both cookies and localStorage to store UID and authentication state.
  • Guests: No cookies are set. No persistent identifiers are stored.
  • We log IP + UID only for abuse protection and quota enforcement.
  • -
  • Uploads are scanned via Whisper+Ollama but not stored as transcripts.
  • Data is never sold.
  • - -
    -
    -

    Account Deletion

    -

    You can delete your account and all associated data at any time. This action is irreversible and will permanently remove:

    -
      -
    • Your account information
    • -
    • All uploaded audio files
    • -
    - -
    - -
    -
    -
    - @@ -175,7 +179,6 @@

    You'll receive a magic login link via email. No password required.

    -

    Your session expires after 1 hour. Shareable links redirect to homepage.

    @@ -217,5 +220,7 @@ } + + diff --git a/static/mobile.css b/static/mobile.css index 819bf80..e6fa6e0 100644 --- a/static/mobile.css +++ b/static/mobile.css @@ -36,11 +36,53 @@ box-sizing: border-box; } - /* Mobile navigation */ - #user-dashboard.dashboard-nav { + /* Mobile navigation - Enhanced with more specific selectors */ + /* Show user dashboard only when authenticated */ + body.authenticated #user-dashboard.dashboard-nav, + html body.authenticated #user-dashboard.dashboard-nav, + body.authenticated #user-dashboard.dashboard-nav:not(.hidden) { display: flex !important; visibility: visible !important; opacity: 1 !important; + height: auto !important; + position: relative !important; + clip: auto !important; + } + + /* Hide guest dashboard when authenticated - with more specific selectors */ + body.authenticated #guest-dashboard.dashboard-nav, + html body.authenticated #guest-dashboard.dashboard-nav, + body.authenticated #guest-dashboard.dashboard-nav:not(.visible) { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + height: 0 !important; + width: 0 !important; + padding: 0 !important; + margin: 0 !important; + border: none !important; + position: absolute !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + } + + /* Show guest dashboard when not authenticated - with more specific selectors */ + body:not(.authenticated) #guest-dashboard.dashboard-nav, + html body:not(.authenticated) #guest-dashboard.dashboard-nav, + body:not(.authenticated) #guest-dashboard.dashboard-nav:not(.hidden) { + display: flex !important; + visibility: visible !important; + opacity: 1 !important; + height: auto !important; + position: relative !important; + } + + /* Ensure user dashboard is hidden when not authenticated */ + body:not(.authenticated) #user-dashboard.dashboard-nav { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + height: 0 !important; } .dashboard-nav {