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
This commit is contained in:
oib
2025-07-20 09:24:51 +02:00
parent c5412b07ac
commit da28b205e5
4 changed files with 479 additions and 132 deletions

View File

@ -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 = '<li class="no-files">No files uploaded yet.</li>';
}
} 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 = `
<div class="file-info">
<span class="file-icon">${fileIcon}</span>
<a href="${fileUrl}" class="file-name" target="_blank" rel="noopener noreferrer">
${fileName}
${displayName}
</a>
<span class="file-size">${fileSize}</span>
</div>
@ -649,18 +750,15 @@ async function fetchAndDisplayFiles(uid) {
<span class="button-icon">⬇️</span>
<span class="button-text">Download</span>
</a>
<button class="delete-button" data-filename="${fileName}">
<button class="delete-file" data-filename="${storedFileName}" data-original-name="${displayName}">
<span class="button-icon">🗑️</span>
<span class="button-text">Delete</span>
</button>
</div>
`;
// 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 = '<span class="button-icon">⏳</span><span class="button-text">Deleting...</span>';
}
}
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 = '<li class="no-files">No files uploaded yet.</li>';
}
} 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 {