Update authentication system, database models, and UI components

This commit is contained in:
oib
2025-08-07 19:39:22 +02:00
parent d497492186
commit 72f79b1059
48 changed files with 5328 additions and 1642 deletions

View File

@ -16,6 +16,14 @@ export class AudioPlayer {
this.lastPlayTime = 0;
this.isLoading = false;
this.loadTimeout = null; // For tracking loading timeouts
this.retryCount = 0;
this.maxRetries = 3;
this.retryDelay = 3000; // 3 seconds
this.buffering = false;
this.bufferRetryTimeout = null;
this.lastLoadTime = 0;
this.minLoadInterval = 2000; // 2 seconds between loads
this.pendingLoad = false;
// Create a single audio element that we'll reuse
this.audioElement = new Audio();
@ -26,6 +34,14 @@ export class AudioPlayer {
this.loadAndPlay = this.loadAndPlay.bind(this);
this.stop = this.stop.bind(this);
this.cleanup = this.cleanup.bind(this);
this.handlePlayError = this.handlePlayError.bind(this);
this.handleStalled = this.handleStalled.bind(this);
this.handleWaiting = this.handleWaiting.bind(this);
this.handlePlaying = this.handlePlaying.bind(this);
this.handleEnded = this.handleEnded.bind(this);
// Set up event listeners
this.setupEventListeners();
// Register with global audio manager to handle stop requests from other players
globalAudioManager.addListener('personal', () => {
@ -63,14 +79,41 @@ export class AudioPlayer {
}
async loadAndPlay(uid, button) {
const now = Date.now();
// Prevent rapid successive load attempts
if (this.pendingLoad || (now - this.lastLoadTime < this.minLoadInterval)) {
console.log('[AudioPlayer] Skipping duplicate load request');
return;
}
// Validate UID exists and is in correct format
if (!uid) {
this.handleError(button, 'No UID provided for audio playback');
return;
}
if (!this.isValidUuid(uid)) {
this.handleError(button, `Invalid UID format: ${uid}. Expected UUID v4 format.`);
// For logging purposes
const requestId = Math.random().toString(36).substr(2, 8);
console.log(`[AudioPlayer] Load request ${requestId} for UID: ${uid}`);
this.pendingLoad = true;
this.lastLoadTime = now;
// If we're in the middle of loading, check if it's for the same UID
if (this.isLoading) {
// If same UID, ignore duplicate request
if (this.currentUid === uid) {
console.log(`[AudioPlayer] Already loading this UID, ignoring duplicate request: ${uid}`);
this.pendingLoad = false;
return;
}
// If different UID, queue the new request
console.log(`[AudioPlayer] Already loading, queuing request for UID: ${uid}`);
setTimeout(() => {
this.pendingLoad = false;
this.loadAndPlay(uid, button);
}, 500);
return;
}
@ -170,8 +213,10 @@ export class AudioPlayer {
// Set the source URL with proper encoding and cache-busting timestamp
// Using the format: /audio/{uid}/stream.opus?t={timestamp}
const timestamp = new Date().getTime();
// Only update timestamp if we're loading a different UID or after a retry
const timestamp = this.retryCount > 0 ? new Date().getTime() : this.lastLoadTime;
this.audioUrl = `/audio/${encodeURIComponent(uid)}/stream.opus?t=${timestamp}`;
console.log(`[AudioPlayer] Loading audio from URL: ${this.audioUrl} (attempt ${this.retryCount + 1}/${this.maxRetries})`);
console.log('Loading audio from URL:', this.audioUrl);
this.audioElement.src = this.audioUrl;
@ -312,10 +357,150 @@ export class AudioPlayer {
}
}
/**
* Set up event listeners for the audio element
*/
setupEventListeners() {
if (!this.audioElement) return;
// Remove any existing listeners to prevent duplicates
this.audioElement.removeEventListener('error', this.handlePlayError);
this.audioElement.removeEventListener('stalled', this.handleStalled);
this.audioElement.removeEventListener('waiting', this.handleWaiting);
this.audioElement.removeEventListener('playing', this.handlePlaying);
this.audioElement.removeEventListener('ended', this.handleEnded);
// Add new listeners
this.audioElement.addEventListener('error', this.handlePlayError);
this.audioElement.addEventListener('stalled', this.handleStalled);
this.audioElement.addEventListener('waiting', this.handleWaiting);
this.audioElement.addEventListener('playing', this.handlePlaying);
this.audioElement.addEventListener('ended', this.handleEnded);
}
/**
* Handle play errors
*/
handlePlayError(event) {
console.error('[AudioPlayer] Playback error:', {
event: event.type,
error: this.audioElement.error,
currentTime: this.audioElement.currentTime,
readyState: this.audioElement.readyState,
networkState: this.audioElement.networkState,
src: this.audioElement.src
});
this.isPlaying = false;
this.buffering = false;
this.pendingLoad = false;
if (this.currentButton) {
this.updateButtonState(this.currentButton, 'error');
}
// Auto-retry logic
if (this.retryCount < this.maxRetries) {
this.retryCount++;
console.log(`Retrying playback (attempt ${this.retryCount}/${this.maxRetries})...`);
setTimeout(() => {
if (this.currentUid && this.currentButton) {
this.loadAndPlay(this.currentUid, this.currentButton);
}
}, this.retryDelay);
} else {
console.error('Max retry attempts reached');
this.retryCount = 0; // Reset for next time
}
}
/**
* Handle stalled audio (buffering issues)
*/
handleStalled() {
console.log('[AudioPlayer] Playback stalled, attempting to recover...');
this.buffering = true;
if (this.bufferRetryTimeout) {
clearTimeout(this.bufferRetryTimeout);
}
this.bufferRetryTimeout = setTimeout(() => {
if (this.buffering) {
console.log('[AudioPlayer] Buffer recovery timeout, attempting to reload...');
if (this.currentUid && this.currentButton) {
// Only retry if we're still supposed to be playing
if (this.isPlaying) {
this.retryCount++;
if (this.retryCount <= this.maxRetries) {
console.log(`[AudioPlayer] Retry ${this.retryCount}/${this.maxRetries} for UID: ${this.currentUid}`);
this.loadAndPlay(this.currentUid, this.currentButton);
} else {
console.error('[AudioPlayer] Max retry attempts reached');
this.retryCount = 0;
this.updateButtonState(this.currentButton, 'error');
}
}
}
}
}, 5000); // 5 second buffer recovery timeout
}
/**
* Handle waiting event (buffering)
*/
handleWaiting() {
console.log('Audio waiting for data...');
this.buffering = true;
if (this.currentButton) {
this.updateButtonState(this.currentButton, 'loading');
}
}
/**
* Handle playing event (playback started/resumed)
*/
handlePlaying() {
console.log('Audio playback started/resumed');
this.buffering = false;
this.retryCount = 0; // Reset retry counter on successful playback
if (this.bufferRetryTimeout) {
clearTimeout(this.bufferRetryTimeout);
this.bufferRetryTimeout = null;
}
if (this.currentButton) {
this.updateButtonState(this.currentButton, 'playing');
}
}
/**
* Handle ended event (playback completed)
*/
handleEnded() {
console.log('Audio playback ended');
this.isPlaying = false;
this.buffering = false;
if (this.currentButton) {
this.updateButtonState(this.currentButton, 'paused');
}
}
/**
* Clean up resources
*/
cleanup() {
// Clear any pending timeouts
if (this.loadTimeout) {
clearTimeout(this.loadTimeout);
this.loadTimeout = null;
}
if (this.bufferRetryTimeout) {
clearTimeout(this.bufferRetryTimeout);
this.bufferRetryTimeout = null;
}
// Update button state if we have a reference to the current button
if (this.currentButton) {
this.updateButtonState(this.currentButton, 'paused');
@ -324,6 +509,13 @@ export class AudioPlayer {
// Pause the audio and store the current time
if (this.audioElement) {
try {
// Remove event listeners to prevent memory leaks
this.audioElement.removeEventListener('error', this.handlePlayError);
this.audioElement.removeEventListener('stalled', this.handleStalled);
this.audioElement.removeEventListener('waiting', this.handleWaiting);
this.audioElement.removeEventListener('playing', this.handlePlaying);
this.audioElement.removeEventListener('ended', this.handleEnded);
try {
this.audioElement.pause();
this.lastPlayTime = this.audioElement.currentTime;
@ -357,6 +549,8 @@ export class AudioPlayer {
this.currentButton = null;
this.audioUrl = '';
this.isPlaying = false;
this.buffering = false;
this.retryCount = 0;
// Notify global audio manager that personal player has stopped
globalAudioManager.stopPlayback('personal');

688
static/auth-manager.js Normal file
View File

@ -0,0 +1,688 @@
/**
* Centralized Authentication Manager
*
* This module consolidates all authentication logic from auth.js, magic-login.js,
* and cleanup-auth.js into a single, maintainable module.
*/
import { showToast } from './toast.js';
class AuthManager {
constructor() {
this.DEBUG_AUTH_STATE = false;
this.AUTH_CHECK_DEBOUNCE = 1000; // 1 second
this.AUTH_CHECK_INTERVAL = 30000; // 30 seconds
this.CACHE_TTL = 5000; // 5 seconds
// Authentication state cache
this.authStateCache = {
timestamp: 0,
value: null,
ttl: this.CACHE_TTL
};
// Track auth check calls
this.lastAuthCheckTime = 0;
this.authCheckCounter = 0;
this.wasAuthenticated = null;
// Bind all methods that will be used as event handlers
this.checkAuthState = this.checkAuthState.bind(this);
this.handleMagicLoginRedirect = this.handleMagicLoginRedirect.bind(this);
this.logout = this.logout.bind(this);
this.deleteAccount = this.deleteAccount.bind(this);
this.handleStorageEvent = this.handleStorageEvent.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
// Initialize
this.initialize = this.initialize.bind(this);
}
/**
* Validate UID format - must be a valid email address
*/
validateUidFormat(uid) {
if (!uid || typeof uid !== 'string') {
// Debug messages disabled
return false;
}
// Email regex pattern - RFC 5322 compliant basic validation
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const isValid = emailRegex.test(uid);
if (!isValid) {
// Debug messages disabled
} else {
// Debug messages disabled
}
return isValid;
}
/**
* Sanitize and validate UID - ensures consistent format
*/
sanitizeUid(uid) {
if (!uid || typeof uid !== 'string') {
// Debug messages disabled
return null;
}
// Trim whitespace and convert to lowercase
const sanitized = uid.trim().toLowerCase();
// Validate the sanitized UID
if (!this.validateUidFormat(sanitized)) {
// Debug messages disabled
return null;
}
// Debug messages disabled
return sanitized;
}
/**
* Check if current stored UID is valid and fix if needed
*/
validateStoredUid() {
const storedUid = localStorage.getItem('uid');
if (!storedUid) {
// Debug messages disabled
return null;
}
const sanitizedUid = this.sanitizeUid(storedUid);
if (!sanitizedUid) {
// Debug messages disabled
this.clearAuthState();
return null;
}
// Update stored UID if sanitization changed it
if (sanitizedUid !== storedUid) {
// Debug messages disabled
localStorage.setItem('uid', sanitizedUid);
// Update cookies as well
document.cookie = `uid=${sanitizedUid}; path=/; SameSite=Lax; Secure`;
}
return sanitizedUid;
}
/**
* Get cookie value by name
*/
getCookieValue(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
return null;
}
/**
* Initialize the authentication manager
*/
async initialize() {
// Debug messages disabled
// Validate stored UID format and fix if needed
const validUid = this.validateStoredUid();
if (validUid) {
// Debug messages disabled
} else {
// Debug messages disabled
}
// Handle magic link login if present
await this.handleMagicLoginRedirect();
// Setup authentication state polling
this.setupAuthStatePolling();
// Setup event listeners
document.addEventListener('visibilitychange', this.handleVisibilityChange);
this.setupEventListeners();
// Debug messages disabled
}
/**
* Fetch user information from the server
*/
async fetchUserInfo() {
try {
// Get the auth token from cookies
const authToken = this.getCookieValue('authToken') || localStorage.getItem('authToken');
// Debug messages disabled
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
// Add Authorization header if we have a token
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
// Debug messages disabled
} else {
// Debug messages disabled
}
// Debug messages disabled
const response = await fetch('/api/me', {
method: 'GET',
credentials: 'include',
headers: headers
});
// Debug messages disabled
if (response.ok) {
const contentType = response.headers.get('content-type');
// Debug messages disabled
if (contentType && contentType.includes('application/json')) {
const userInfo = await response.json();
// Debug messages disabled
return userInfo;
} else {
const text = await response.text();
// Debug messages disabled
}
} else {
const errorText = await response.text();
// Debug messages disabled
}
return null;
} catch (error) {
// Debug messages disabled
return null;
}
}
/**
* Set authentication state in localStorage and cookies
*/
setAuthState(userEmail, username, authToken = null) {
// Debug messages disabled
// Validate and sanitize the UID (email)
const sanitizedUid = this.sanitizeUid(userEmail);
if (!sanitizedUid) {
// Debug messages disabled
throw new Error(`Invalid UID format: ${userEmail}. UID must be a valid email address.`);
}
// Validate username (basic check)
if (!username || typeof username !== 'string' || username.trim().length === 0) {
// Debug messages disabled
throw new Error(`Invalid username: ${username}. Username cannot be empty.`);
}
const sanitizedUsername = username.trim();
// Generate auth token if not provided
if (!authToken) {
authToken = 'token-' + Math.random().toString(36).substring(2, 15);
}
// Debug messages disabled
// Set localStorage for client-side access (not sent to server)
localStorage.setItem('uid', sanitizedUid); // Primary UID is email
localStorage.setItem('username', sanitizedUsername); // Username for display
localStorage.setItem('uid_time', Date.now().toString());
// Set cookies for server authentication (sent with requests)
document.cookie = `uid=${encodeURIComponent(sanitizedUid)}; path=/; SameSite=Lax`;
document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
// Note: isAuthenticated is determined by presence of valid authToken, no need to duplicate
// Clear cache to force refresh
this.authStateCache.timestamp = 0;
}
/**
* Clear authentication state
*/
clearAuthState() {
// Debug messages disabled
// Clear localStorage (client-side data only)
const authKeys = ['uid', 'username', 'uid_time'];
authKeys.forEach(key => localStorage.removeItem(key));
// Clear cookies
document.cookie.split(';').forEach(cookie => {
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/; SameSite=Lax`;
});
// Clear cache
this.authStateCache.timestamp = 0;
}
/**
* Check if user is currently authenticated
*/
isAuthenticated() {
const now = Date.now();
// Use cached value if still valid
if (this.authStateCache.timestamp > 0 &&
(now - this.authStateCache.timestamp) < this.authStateCache.ttl) {
return this.authStateCache.value;
}
// Check authentication state - simplified approach
const hasUid = !!(document.cookie.includes('uid=') || localStorage.getItem('uid'));
const hasAuthToken = !!document.cookie.includes('authToken=');
const isAuth = hasUid && hasAuthToken;
// Update cache
this.authStateCache.timestamp = now;
this.authStateCache.value = isAuth;
return isAuth;
}
/**
* Get current user data
*/
getCurrentUser() {
if (!this.isAuthenticated()) {
return null;
}
return {
uid: localStorage.getItem('uid'),
email: localStorage.getItem('uid'), // uid is the email
username: localStorage.getItem('username'),
authToken: this.getCookieValue('authToken') // authToken is in cookies
};
}
/**
* Handle magic link login redirect
*/
async handleMagicLoginRedirect() {
const params = new URLSearchParams(window.location.search);
// Handle secure token-based magic login only
const token = params.get('token');
if (token) {
// Debug messages disabled
// Clean up URL immediately
const url = new URL(window.location.href);
url.searchParams.delete('token');
window.history.replaceState({}, document.title, url.pathname + url.search);
await this.processTokenLogin(token);
return true;
}
return false;
}
/**
* Process token-based login
*/
async processTokenLogin(token) {
try {
// Debug messages disabled
const formData = new FormData();
formData.append('token', token);
// Debug messages disabled
const response = await fetch('/magic-login', {
method: 'POST',
body: formData,
});
// Debug messages disabled
// Handle successful token login response
const contentType = response.headers.get('content-type');
// Debug messages disabled
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
// Debug messages disabled
if (data && data.success && data.user) {
// Debug messages disabled
// Use the user data and token from the response
const { email, username } = data.user;
const authToken = data.token; // Get token from JSON response
// Debug messages disabled
// Set auth state with the token from the response
this.setAuthState(email, username, authToken);
this.updateUIState(true);
await this.initializeUserSession(username, email);
showToast('✅ Login successful!');
this.navigateToProfile();
return;
} else {
// Debug messages disabled
throw new Error('Invalid user data received from server');
}
} else {
const text = await response.text();
// Debug messages disabled
throw new Error(`Unexpected response format: ${text || 'No details available'}`);
}
} catch (error) {
// Debug messages disabled
showToast(`Login failed: ${error.message}`, 'error');
}
}
/**
* Initialize user session after login
*/
async initializeUserSession(username, userEmail) {
// Initialize dashboard
if (window.initDashboard) {
await window.initDashboard(username);
} else {
// Debug messages disabled
}
// Fetch and display file list
if (window.fetchAndDisplayFiles) {
// Debug messages disabled
await window.fetchAndDisplayFiles(userEmail);
} else {
// Debug messages disabled
}
}
/**
* Navigate to user profile
*/
navigateToProfile() {
if (window.showOnly) {
// Debug messages disabled
window.showOnly('me-page');
} else if (window.location.hash !== '#me-page') {
window.location.hash = '#me-page';
}
}
/**
* Update UI state based on authentication
*/
updateUIState(isAuthenticated) {
if (isAuthenticated) {
document.body.classList.add('authenticated');
document.body.classList.remove('guest');
// Note: Removed auto-loading of profile stream to prevent auto-play on page load
// Profile stream will only play when user clicks the play button
} else {
document.body.classList.remove('authenticated');
document.body.classList.add('guest');
}
this.updateAccountDeletionVisibility(isAuthenticated);
// Force reflow
void document.body.offsetHeight;
}
/**
* Update account deletion section visibility
*/
updateAccountDeletionVisibility(isAuthenticated) {
const accountDeletionSection = document.getElementById('account-deletion-section');
const deleteAccountFromPrivacy = document.getElementById('delete-account-from-privacy');
if (isAuthenticated) {
this.showElement(accountDeletionSection);
this.showElement(deleteAccountFromPrivacy);
} else {
this.hideElement(accountDeletionSection);
this.hideElement(deleteAccountFromPrivacy);
}
}
showElement(element) {
if (element) {
element.style.display = 'block';
element.style.visibility = 'visible';
}
}
hideElement(element) {
if (element) {
element.style.display = 'none';
}
}
/**
* Check authentication state with caching and debouncing
*/
checkAuthState(force = false) {
const now = Date.now();
// Debounce frequent calls
if (!force && (now - this.lastAuthCheckTime) < this.AUTH_CHECK_DEBOUNCE) {
return this.authStateCache.value;
}
this.lastAuthCheckTime = now;
this.authCheckCounter++;
if (this.DEBUG_AUTH_STATE) {
// Debug messages disabled
}
const isAuthenticated = this.isAuthenticated();
// Only update UI if state changed or forced
if (force || this.wasAuthenticated !== isAuthenticated) {
if (this.DEBUG_AUTH_STATE) {
// Debug messages disabled
}
// Handle logout detection
if (this.wasAuthenticated === true && isAuthenticated === false) {
// Debug messages disabled
this.logout();
return false;
}
this.updateUIState(isAuthenticated);
this.wasAuthenticated = isAuthenticated;
}
return isAuthenticated;
}
/**
* Setup authentication state polling
*/
setupAuthStatePolling() {
// Initial check
this.checkAuthState(true);
// Periodic checks
setInterval(() => {
this.checkAuthState(!document.hidden);
}, this.AUTH_CHECK_INTERVAL);
// Storage event listener
window.addEventListener('storage', this.handleStorageEvent);
// Visibility change listener
document.addEventListener('visibilitychange', this.handleVisibilityChange);
}
/**
* Handle storage events
*/
handleStorageEvent(e) {
if (['isAuthenticated', 'authToken', 'uid'].includes(e.key)) {
this.checkAuthState(true);
}
}
/**
* Handle visibility change events
*/
handleVisibilityChange() {
if (!document.hidden) {
this.checkAuthState(true);
}
}
/**
* Setup event listeners
*/
setupEventListeners() {
document.addEventListener('click', (e) => {
// Delete account buttons
if (e.target.closest('#delete-account') || e.target.closest('#delete-account-from-privacy')) {
this.deleteAccount(e);
return;
}
});
}
/**
* Delete user account
*/
async deleteAccount(e) {
if (e) e.preventDefault();
if (this.deleteAccount.inProgress) return;
if (!confirm('Are you sure you want to delete your account?\nThis action is permanent.')) {
return;
}
this.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');
this.logout();
} catch (error) {
// Debug messages disabled
showToast(error.message, 'error');
} finally {
this.deleteAccount.inProgress = false;
if (deleteBtn) {
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
}
}
}
/**
* Logout user
*/
logout() {
// Debug messages disabled
this.clearAuthState();
window.location.href = '/';
}
/**
* Cleanup authentication state (for migration/debugging)
*/
async cleanupAuthState(manualEmail = null) {
// Debug messages disabled
let userEmail = manualEmail;
// Try to get email from server if not provided
if (!userEmail) {
const userInfo = await this.fetchUserInfo();
userEmail = userInfo?.email;
if (!userEmail) {
userEmail = prompt('Please enter your email address (e.g., oib@chello.at):');
if (!userEmail || !userEmail.includes('@')) {
// Debug messages disabled
return { success: false, error: 'Invalid email' };
}
}
}
if (!userEmail) {
// Debug messages disabled
return { success: false, error: 'No email available' };
}
// Get current username for reference
const currentUsername = localStorage.getItem('username') || localStorage.getItem('uid');
// Clear and reset authentication state
this.clearAuthState();
this.setAuthState(userEmail, currentUsername || userEmail);
// Debug messages disabled
// Debug messages disabled
// Refresh if on profile page
if (window.location.hash === '#me-page') {
window.location.reload();
}
return {
email: userEmail,
username: currentUsername,
success: true
};
}
/**
* Destroy the authentication manager
*/
destroy() {
window.removeEventListener('storage', this.handleStorageEvent);
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
}
}
// Create and export singleton instance
const authManager = new AuthManager();
// Export for global access
window.authManager = authManager;
export default authManager;

View File

@ -1,252 +1,31 @@
import { showToast } from './toast.js';
/**
* Simplified Authentication Module
*
* This file now uses the centralized AuthManager for all authentication logic.
* Legacy code has been replaced with the new consolidated approach.
*/
import authManager from './auth-manager.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();
// Initialize authentication manager when DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
// Debug messages disabled
// Initialize the centralized auth manager
await authManager.initialize();
// Make loadProfileStream available globally for auth manager
window.loadProfileStream = loadProfileStream;
// Debug messages disabled
});
// Export auth manager for other modules to use
export { authManager };
// Legacy compatibility - expose some functions globally
window.getCurrentUser = () => authManager.getCurrentUser();
window.isAuthenticated = () => authManager.isAuthenticated();
window.logout = () => authManager.logout();
window.cleanupAuthState = (email) => authManager.cleanupAuthState(email);

38
static/cleanup-auth.js Normal file
View File

@ -0,0 +1,38 @@
/**
* Simplified Authentication Cleanup Module
*
* This file now uses the centralized AuthManager for authentication cleanup.
* The cleanup logic has been moved to the AuthManager.
*/
import authManager from './auth-manager.js';
/**
* Clean up authentication state - now delegated to AuthManager
* This function is kept for backward compatibility.
*/
async function cleanupAuthState(manualEmail = null) {
console.log('[CLEANUP] Starting authentication state cleanup via AuthManager...');
// Delegate to the centralized AuthManager
return await authManager.cleanupAuthState(manualEmail);
}
// Auto-run cleanup if this script is loaded directly
if (typeof window !== 'undefined') {
// Export function for manual use
window.cleanupAuthState = cleanupAuthState;
// Auto-run if URL contains cleanup parameter
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cleanup') === 'auth') {
cleanupAuthState().then(result => {
if (result && result.success) {
console.log('[CLEANUP] Auto-cleanup completed successfully');
}
});
}
}
// Export for ES6 modules
export { cleanupAuthState };

View File

@ -34,8 +34,7 @@
#file-list li {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
padding: 0.75rem 1rem;
margin: 0.5rem 0;
background-color: var(--surface);
@ -97,36 +96,58 @@
.file-info {
display: flex;
align-items: center;
align-items: flex-start;
flex: 1;
min-width: 0; /* Allows text truncation */
min-width: 0;
flex-direction: column;
gap: 0.25rem;
}
.file-icon {
margin-right: 0.75rem;
font-size: 1.2em;
flex-shrink: 0;
.file-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
gap: 0.75rem;
}
.file-name {
color: var(--primary);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 0.5rem;
}
.file-name:hover {
text-decoration: underline;
color: var(--text-color);
word-break: break-word;
overflow-wrap: break-word;
line-height: 1.3;
flex: 1;
font-size: 0.95em;
}
.file-size {
color: var(--text-muted);
font-size: 0.85em;
margin-left: 0.5rem;
font-size: 0.8em;
white-space: nowrap;
flex-shrink: 0;
font-style: italic;
align-self: flex-start;
}
.delete-file {
align-self: center;
background: none;
border: none;
font-size: 1.1em;
cursor: pointer;
padding: 0.3rem 0.5rem;
border-radius: 4px;
transition: all 0.2s ease;
color: var(--text-muted);
margin-top: 0.2rem;
}
.delete-file:hover {
background-color: var(--error);
color: white;
transform: scale(1.1);
}
.file-actions {

View File

@ -12,13 +12,14 @@ function getCookie(name) {
// Global state
let isLoggingOut = false;
let dashboardInitialized = false;
async function handleLogout(event) {
console.log('[LOGOUT] Logout initiated');
// Debug messages disabled
// Prevent multiple simultaneous logout attempts
if (isLoggingOut) {
console.log('[LOGOUT] Logout already in progress');
// Debug messages disabled
return;
}
isLoggingOut = true;
@ -34,11 +35,11 @@ async function handleLogout(event) {
const authToken = localStorage.getItem('authToken');
// 1. Clear all client-side state first (most important)
console.log('[LOGOUT] Clearing all client-side state');
// Debug messages disabled
// Clear localStorage and sessionStorage
const storageKeys = [
'uid', 'uid_time', 'confirmed_uid', 'last_page',
'uid', 'uid_time', 'last_page',
'isAuthenticated', 'authToken', 'user', 'token', 'sessionid', 'sessionId'
];
@ -49,22 +50,22 @@ async function handleLogout(event) {
// Get all current cookies for debugging
const allCookies = document.cookie.split(';');
console.log('[LOGOUT] Current cookies before clearing:', allCookies);
// Debug messages disabled
// Clear ALL cookies (aggressive approach)
allCookies.forEach(cookie => {
const [name] = cookie.trim().split('=');
if (name) {
const cookieName = name.trim();
console.log(`[LOGOUT] Clearing cookie: ${cookieName}`);
// Debug messages disabled
// Try multiple clearing strategies to ensure cookies are removed
const clearStrategies = [
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname};`,
`${cookieName}=; max-age=0; path=/;`,
`${cookieName}=; max-age=0; path=/; domain=${window.location.hostname};`
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax;`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=Lax;`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname}; SameSite=Lax;`,
`${cookieName}=; max-age=0; path=/; SameSite=Lax;`,
`${cookieName}=; max-age=0; path=/; domain=${window.location.hostname}; SameSite=Lax;`
];
clearStrategies.forEach(strategy => {
@ -75,7 +76,7 @@ async function handleLogout(event) {
// Verify cookies are cleared
const remainingCookies = document.cookie.split(';').filter(c => c.trim());
console.log('[LOGOUT] Remaining cookies after clearing:', remainingCookies);
// Debug messages disabled
// Update UI state
document.body.classList.remove('authenticated', 'logged-in');
@ -84,7 +85,7 @@ async function handleLogout(event) {
// 2. Try to invalidate server session (non-blocking)
if (authToken) {
try {
console.log('[LOGOUT] Attempting to invalidate server session');
// Debug messages disabled
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
@ -99,18 +100,18 @@ async function handleLogout(event) {
});
clearTimeout(timeoutId);
console.log('[LOGOUT] Server session invalidation completed');
// Debug messages disabled
} catch (error) {
console.warn('[LOGOUT] Server session invalidation failed (non-critical):', error);
// Debug messages disabled
}
}
// 3. Final redirect
console.log('[LOGOUT] Redirecting to home page');
// Debug messages disabled
window.location.href = '/?logout=' + Date.now();
} catch (error) {
console.error('[LOGOUT] Unexpected error during logout:', error);
// Debug messages disabled
if (window.showToast) {
showToast('Logout failed. Please try again.');
}
@ -138,7 +139,7 @@ async function handleDeleteAccount() {
}
// Show loading state
const deleteButton = document.getElementById('delete-account-button');
const deleteButton = document.getElementById('delete-account-from-privacy');
const originalText = deleteButton.textContent;
deleteButton.disabled = true;
deleteButton.textContent = 'Deleting...';
@ -162,7 +163,7 @@ async function handleDeleteAccount() {
// Clear all authentication-related data from localStorage
const keysToRemove = [
'uid', 'uid_time', 'confirmed_uid', 'last_page',
'uid', 'uid_time', 'last_page',
'isAuthenticated', 'authToken', 'user', 'token', 'sessionid'
];
@ -180,11 +181,11 @@ async function handleDeleteAccount() {
// Clear all cookies using multiple strategies
const clearCookie = (cookieName) => {
const clearStrategies = [
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname};`,
`${cookieName}=; max-age=0; path=/;`,
`${cookieName}=; max-age=0; path=/; domain=${window.location.hostname};`
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax;`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=Lax;`,
`${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${window.location.hostname}; SameSite=Lax;`,
`${cookieName}=; max-age=0; path=/; SameSite=Lax;`,
`${cookieName}=; max-age=0; path=/; domain=${window.location.hostname}; SameSite=Lax;`
];
clearStrategies.forEach(strategy => {
@ -224,7 +225,7 @@ async function handleDeleteAccount() {
showToast(`Failed to delete account: ${error.message}`);
// Reset button state
const deleteButton = document.getElementById('delete-account-button');
const deleteButton = document.getElementById('delete-account-from-privacy');
if (deleteButton) {
deleteButton.disabled = false;
deleteButton.textContent = '🗑️ Delete Account';
@ -251,33 +252,37 @@ function debugElementVisibility(elementId) {
parentDisplay: el.parentElement ? window.getComputedStyle(el.parentElement).display : 'no-parent',
parentVisibility: el.parentElement ? window.getComputedStyle(el.parentElement).visibility : 'no-parent',
rect: el.getBoundingClientRect()
};
}
}
// Make updateQuotaDisplay available globally
window.updateQuotaDisplay = updateQuotaDisplay;
/**
* Initialize the dashboard and handle authentication state
*/
async function initDashboard() {
console.log('[DASHBOARD] Initializing dashboard...');
async function initDashboard(uid = null) {
// Debug messages disabled
try {
const guestDashboard = document.getElementById('guest-dashboard');
const userDashboard = document.getElementById('user-dashboard');
const userUpload = document.getElementById('user-upload-area');
const logoutButton = document.getElementById('logout-button');
const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountButton = document.getElementById('delete-account-from-privacy');
const fileList = document.getElementById('file-list');
if (logoutButton) {
logoutButton.addEventListener('click', handleLogout);
}
if (deleteAccountButton) {
deleteAccountButton.addEventListener('click', (e) => {
e.preventDefault();
handleDeleteAccount();
});
// Only attach event listeners once to prevent duplicates
if (!dashboardInitialized) {
if (logoutButton) {
logoutButton.addEventListener('click', handleLogout);
}
// Delete account button is handled by auth.js delegated event listener
// Removed duplicate event listener to prevent double confirmation dialogs
dashboardInitialized = true;
}
const isAuthenticated = (document.cookie.includes('isAuthenticated=true') || localStorage.getItem('isAuthenticated') === 'true');
const effectiveUid = uid || getCookie('uid') || localStorage.getItem('uid');
const isAuthenticated = !!effectiveUid;
if (isAuthenticated) {
document.body.classList.add('authenticated');
@ -286,9 +291,11 @@ async function initDashboard() {
if (userUpload) userUpload.style.display = 'block';
if (guestDashboard) guestDashboard.style.display = 'none';
const uid = getCookie('uid') || localStorage.getItem('uid');
if (uid && window.fetchAndDisplayFiles) {
await window.fetchAndDisplayFiles(uid);
if (window.fetchAndDisplayFiles) {
// Use email-based UID for file operations if available, fallback to effectiveUid
const fileOperationUid = localStorage.getItem('uid') || effectiveUid; // uid is now email-based
// Debug messages disabled
await window.fetchAndDisplayFiles(fileOperationUid);
}
} else {
document.body.classList.remove('authenticated');
@ -297,7 +304,7 @@ async function initDashboard() {
if (userDashboard) userDashboard.style.display = 'none';
if (userUpload) userUpload.style.display = 'none';
if (fileList) {
fileList.innerHTML = `<li class="error-message">Please <a href="/#login" class="login-link">log in</a> to view your files.</li>`;
fileList.innerHTML = `<li>Please <a href="/#login" class="login-link">log in</a> to view your files.</li>`;
}
}
} catch (e) {
@ -326,11 +333,11 @@ async function fetchAndDisplayFiles(uid) {
const fileList = document.getElementById('file-list');
if (!fileList) {
console.error('[FILES] File list element not found');
// Debug messages disabled
return;
}
console.log(`[FILES] Fetching files for user: ${uid}`);
// Debug messages disabled
fileList.innerHTML = '<li class="loading-message">Loading your files...</li>';
// Prepare headers with auth token if available
@ -344,44 +351,44 @@ async function fetchAndDisplayFiles(uid) {
headers['Authorization'] = `Bearer ${authToken}`;
}
console.log('[FILES] Making request to /me with headers:', headers);
// Debug messages disabled
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/${uid} with credentials...`);
const response = await fetch(`/me/${uid}`, {
// Debug messages disabled
const response = await fetch(`/user-files/${uid}`, {
method: 'GET',
credentials: 'include', // Important: include cookies for session auth
headers: headers
});
console.log('[FILES] Response status:', response.status);
console.log('[FILES] Response headers:', Object.fromEntries([...response.headers.entries()]));
// Debug messages disabled
// Debug messages disabled
// Get response as text first to handle potential JSON parsing errors
const responseText = await response.text();
console.log('[FILES] Raw response text:', responseText);
// Debug messages disabled
// Parse the JSON response
let responseData = {};
if (responseText && responseText.trim() !== '') {
try {
responseData = JSON.parse(responseText);
console.log('[FILES] Successfully parsed JSON response:', responseData);
// Debug messages disabled
} catch (e) {
console.error('[FILES] Failed to parse JSON response. Response text:', responseText);
console.error('[FILES] Error details:', e);
// Debug messages disabled
// Debug messages disabled
// If we have a non-JSON response but the status is 200, try to handle it
if (response.ok) {
console.warn('[FILES] Non-JSON response with 200 status, treating as empty response');
// Debug messages disabled
} else {
throw new Error(`Invalid JSON response from server: ${e.message}`);
}
}
} else {
console.log('[FILES] Empty response received, using empty object');
// Debug messages disabled
}
// Note: Authentication is handled by the parent component
@ -390,13 +397,13 @@ async function fetchAndDisplayFiles(uid) {
if (response.ok) {
// Check if the response has the expected format
if (!responseData || !Array.isArray(responseData.files)) {
console.error('[FILES] Invalid response format, expected {files: [...]}:', responseData);
// Debug messages disabled
fileList.innerHTML = '<li>Error: Invalid response from server</li>';
return;
}
const files = responseData.files;
console.log('[FILES] Files array:', files);
// Debug messages disabled
if (files.length === 0) {
fileList.innerHTML = '<li class="no-files">No files uploaded yet.</li>';
@ -406,68 +413,9 @@ 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(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
if (['mp3', 'wav', 'ogg', 'm4a', 'opus'].includes(fileExt)) {
fileIcon = '🎵';
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt)) {
fileIcon = '🖼️';
} else if (['pdf', 'doc', 'docx', 'txt'].includes(fileExt)) {
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">
${displayName}
</a>
<span class="file-size">${fileSize}</span>
</div>
<div class="file-actions">
<a href="${fileUrl}" class="download-button" download>
<span class="button-icon">⬇️</span>
<span class="button-text">Download</span>
</a>
<button class="delete-file" data-filename="${storedFileName}" data-original-name="${displayName}">
<span class="button-icon">🗑️</span>
<span class="button-text">Delete</span>
</button>
</div>
`;
// Delete button handler will be handled by event delegation
// No need to add individual event listeners here
fileList.appendChild(listItem);
});
// Use the new global function to render the files
window.displayUserFiles(uid, files);
} else {
// Handle non-OK responses
if (response.status === 401) {
@ -482,10 +430,10 @@ async function fetchAndDisplayFiles(uid) {
Error loading files (${response.status}). Please try again later.
</li>`;
}
console.error('[FILES] Server error:', response.status, response.statusText);
// Debug messages disabled
}
} catch (error) {
console.error('[FILES] Error fetching files:', error);
// Debug messages disabled
const fileList = document.getElementById('file-list');
if (fileList) {
fileList.innerHTML = `
@ -496,6 +444,69 @@ async function fetchAndDisplayFiles(uid) {
}
}
// Function to update the quota display
async function updateQuotaDisplay(uid) {
// Debug messages disabled
try {
const authToken = localStorage.getItem('authToken');
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
// Debug messages disabled
// Fetch user info which includes quota
const response = await fetch(`/me/${uid}`, {
method: 'GET',
credentials: 'include',
headers: headers
});
// Debug messages disabled
if (response.ok) {
const userData = await response.json();
// Debug messages disabled
// Update the quota display
const quotaText = document.getElementById('quota-text');
const quotaBar = document.getElementById('quota-bar');
// Debug messages disabled
// Debug messages disabled
if (quotaText && userData.quota) {
const usedMB = (userData.quota.used_bytes / (1024 * 1024)).toFixed(2);
const maxMB = (userData.quota.max_bytes / (1024 * 1024)).toFixed(2);
const percentage = userData.quota.percentage || 0;
// Debug messages disabled
const quotaDisplayText = `${usedMB} MB of ${maxMB} MB (${percentage}%)`;
quotaText.textContent = quotaDisplayText;
// Debug messages disabled
if (quotaBar) {
quotaBar.value = percentage;
// Debug messages disabled
}
} else {
// Debug messages disabled
}
} else {
// Debug messages disabled
}
} catch (error) {
// Debug messages disabled
}
}
// Make fetchAndDisplayFiles globally accessible
window.fetchAndDisplayFiles = fetchAndDisplayFiles;
// Function to handle file deletion
async function deleteFile(uid, fileName, listItem, displayName = '') {
const fileToDelete = displayName || fileName;
@ -519,7 +530,7 @@ async function deleteFile(uid, fileName, listItem, displayName = '') {
throw new Error('User not authenticated. Please log in again.');
}
console.log(`[DELETE] Attempting to delete file: ${fileName} for user: ${uid}`);
// Debug messages disabled
const authToken = localStorage.getItem('authToken');
const headers = { 'Content-Type': 'application/json' };
@ -553,7 +564,7 @@ async function deleteFile(uid, fileName, listItem, displayName = '') {
fileList.innerHTML = '<li class="no-files">No files uploaded yet.</li>';
}
} catch (error) {
console.error('[DELETE] Error deleting file:', error);
// Debug messages disabled
showToast(`Error deleting "${fileToDelete}": ${error.message}`, 'error');
// Reset the button state if there was an error
@ -575,7 +586,7 @@ function initFileUpload() {
const fileInput = document.getElementById('fileInputUser');
if (!uploadArea || !fileInput) {
console.warn('[UPLOAD] Required elements not found for file upload');
// Debug messages disabled
return;
}
@ -630,7 +641,7 @@ function initFileUpload() {
}
} catch (error) {
console.error('[UPLOAD] Error uploading file:', error);
// Debug messages disabled
showToast(`Upload failed: ${error.message}`, 'error');
} finally {
// Reset file input and restore upload area text
@ -679,9 +690,15 @@ function initFileUpload() {
}
// Main initialization when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
// Initialize dashboard components
initDashboard(); // initFileUpload is called from within initDashboard
await initDashboard(); // initFileUpload is called from within initDashboard
// Update quota display if user is logged in
const uid = localStorage.getItem('uid');
if (uid) {
updateQuotaDisplay(uid);
}
// Delegated event listener for clicks on the document
document.addEventListener('click', (e) => {
@ -701,10 +718,10 @@ document.addEventListener('DOMContentLoaded', () => {
const listItem = deleteButton.closest('.file-item');
if (!listItem) return;
const uid = localStorage.getItem('uid') || localStorage.getItem('confirmed_uid');
const uid = localStorage.getItem('uid');
if (!uid) {
showToast('You need to be logged in to delete files', 'error');
console.error('[DELETE] No UID found in localStorage');
// Debug messages disabled
return;
}
@ -715,8 +732,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Make fetchAndDisplayFiles available globally
// Make dashboard functions available globally
window.fetchAndDisplayFiles = fetchAndDisplayFiles;
window.initDashboard = initDashboard;
// Login/Register (guest)
const regForm = document.getElementById('register-form');
@ -757,7 +775,7 @@ document.addEventListener('DOMContentLoaded', () => {
regForm.reset();
} else {
showToast(`Error: ${data.detail || 'Unknown error occurred'}`, 'error');
console.error('Registration failed:', data);
// Debug messages disabled
}
} catch (parseError) {
console.error('Error parsing response:', parseError);

220
static/file-display.js Normal file
View File

@ -0,0 +1,220 @@
// This function is responsible for rendering the list of files to the DOM.
// It is globally accessible via window.displayUserFiles.
window.displayUserFiles = function(uid, files) {
const fileList = document.getElementById('file-list');
if (!fileList) {
// Debug messages disabled
return;
}
if (!files || files.length === 0) {
fileList.innerHTML = '<li>You have no uploaded files yet.</li>';
return;
}
const fragment = document.createDocumentFragment();
const displayedFiles = new Set();
files.forEach(file => {
// Use original_name for display, stored_name for operations.
let displayName = file.original_name || file.stored_name || 'Unnamed File';
const storedFileName = file.stored_name || file.original_name;
// No UUID pattern replacement: always show the original_name from backend.
// Skip if no valid identifier is found or if it's a duplicate.
if (!storedFileName || displayedFiles.has(storedFileName)) {
return;
}
displayedFiles.add(storedFileName);
const listItem = document.createElement('li');
const fileUrl = `/user-uploads/${uid}/${encodeURIComponent(storedFileName)}`;
const fileSize = file.size ? (file.size / 1024 / 1024).toFixed(2) + ' MB' : 'N/A';
let fileIcon = '🎵'; // Default icon
const fileExt = displayName.split('.').pop().toLowerCase();
if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(fileExt)) {
fileIcon = '🎵';
} else if (['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(fileExt)) {
fileIcon = '🖼️';
} else if (['pdf', 'doc', 'docx', 'txt'].includes(fileExt)) {
fileIcon = '📄';
}
listItem.innerHTML = `
<div class="file-info">
<div class="file-header">
<span class="file-name">${displayName}</span>
<span class="file-size">${fileSize}</span>
</div>
</div>
<button class="delete-file" title="Delete file" data-filename="${storedFileName}" data-display-name="${displayName}">🗑️</button>
`;
fragment.appendChild(listItem);
});
fileList.appendChild(fragment);
};
// Function to handle file deletion
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.textContent = '⏳';
}
}
try {
if (!uid) {
throw new Error('User not authenticated. Please log in again.');
}
// Debug messages disabled
const authToken = localStorage.getItem('authToken');
const headers = { 'Content-Type': 'application/json' };
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
// Get the email from localStorage (it's the UID)
const email = localStorage.getItem('uid');
if (!email) {
throw new Error('User not authenticated');
}
// The backend expects the full email as the UID in the path
// We need to ensure it's properly encoded for the URL
const username = email;
// Debug messages disabled
// Check if the filename is just a UUID (without log ID prefix)
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.\w+$/i;
let fileToDelete = fileName;
// If the filename is just a UUID, try to find the actual file with log ID prefix
if (uuidPattern.test(fileName)) {
// Debug messages disabled
try {
// First try to get the list of files to find the one with the matching UUID
const filesResponse = await fetch(`/user-files/${uid}`, {
method: 'GET',
headers: headers,
credentials: 'include'
});
if (filesResponse.ok) {
const filesData = await filesResponse.json();
if (filesData.files && Array.isArray(filesData.files)) {
// Look for a file that contains our UUID in its name
const matchingFile = filesData.files.find(f =>
f.stored_name && f.stored_name.includes(fileName)
);
if (matchingFile && matchingFile.stored_name) {
// Debug messages disabled
fileToDelete = matchingFile.stored_name;
}
}
}
} catch (e) {
// Debug messages disabled
// Continue with the original filename if there's an error
}
}
// Use the username in the URL with the correct filename
// Debug messages disabled
const response = await fetch(`/uploads/${username}/${encodeURIComponent(fileToDelete)}`, {
method: 'DELETE',
headers: headers,
credentials: 'include'
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
// Remove the file from the UI immediately
if (listItem && listItem.parentNode) {
listItem.parentNode.removeChild(listItem);
}
// Show success message
window.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>';
}
// Refresh the file list and stream
const uid_current = localStorage.getItem('uid');
if (window.fetchAndDisplayFiles) {
// Use email-based UID for file operations if available, fallback to uid_current
const fileOperationUid = localStorage.getItem('uid') || uid_current; // uid is now email-based
// Debug messages disabled
await window.fetchAndDisplayFiles(fileOperationUid);
}
if (window.loadProfileStream) {
await window.loadProfileStream(uid_current);
}
} catch (error) {
// Debug messages disabled
window.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.textContent = '🗑️';
}
}
}
}
// Add event delegation for delete buttons
document.addEventListener('DOMContentLoaded', () => {
const fileList = document.getElementById('file-list');
if (fileList) {
fileList.addEventListener('click', (e) => {
const deleteButton = e.target.closest('.delete-file');
if (deleteButton) {
e.preventDefault();
e.stopPropagation();
const listItem = deleteButton.closest('li');
if (!listItem) return;
const uid = localStorage.getItem('uid');
if (!uid) {
window.showToast('You need to be logged in to delete files', 'error');
// Debug messages disabled
return;
}
const fileName = deleteButton.getAttribute('data-filename');
const displayName = deleteButton.getAttribute('data-display-name') || fileName;
deleteFile(uid, fileName, listItem, displayName);
}
});
}
});

View File

@ -23,7 +23,7 @@ class GlobalAudioManager {
* @param {Object} playerInstance - Reference to the player instance
*/
startPlayback(playerType, uid, playerInstance = null) {
console.log(`[GlobalAudioManager] startPlayback called by: ${playerType} for UID: ${uid}`);
// Debug messages disabled
// If the same player is already playing the same UID, allow it
if (this.currentPlayer === playerType && this.currentUid === uid) {
return true;
@ -38,7 +38,7 @@ class GlobalAudioManager {
this.currentPlayer = playerType;
this.currentUid = uid;
console.log(`Global Audio Manager: ${playerType} player started playing UID: ${uid}`);
// Debug messages disabled
return true;
}
@ -48,7 +48,7 @@ class GlobalAudioManager {
*/
stopPlayback(playerType) {
if (this.currentPlayer === playerType) {
console.log(`Global Audio Manager: ${playerType} player stopped`);
// Debug messages disabled
this.currentPlayer = null;
this.currentUid = null;
}
@ -93,7 +93,7 @@ class GlobalAudioManager {
* Notify a specific player type to stop
*/
notifyStop(playerType) {
console.log(`Global Audio Manager: Notifying ${playerType} player to stop`);
// Debug messages disabled
this.listeners.forEach(listener => {
if (listener.playerType === playerType) {
try {

View File

@ -21,9 +21,11 @@
}
</style>
<link rel="modulepreload" href="/static/sound.js" />
<script src="/static/file-display.js?v=3"></script>
<script type="module" src="/static/dashboard.js?v=7"></script>
<script src="/static/streams-ui.js?v=3" type="module"></script>
<script src="/static/auth.js?v=2" type="module"></script>
<script src="/static/app.js?v=5" type="module"></script>
<script src="/static/auth.js?v=5" type="module"></script>
<script src="/static/app.js?v=6" type="module"></script>
</head>
<body>
<header>
@ -66,12 +68,12 @@
<button id="logout-button" class="button">🚪 Log Out</button>
</article>
<section id="quota-meter" class="auth-only">
<p class="quota-meter">Quota: <progress id="quota-bar" value="0" max="100"></progress> <span id="quota-text">0 MB</span></p>
<h4>Uploaded Files</h4>
<section id="uploaded-files" class="auth-only">
<h3>Uploaded Files</h3>
<ul id="file-list" class="file-list">
<li>Loading files...</li>
</ul>
<p class="quota-meter">Quota: <progress id="quota-bar" value="0" max="100"></progress> <span id="quota-text">0 MB</span></p>
</section>
<!-- Account Deletion Section -->
@ -194,7 +196,6 @@
</p>
</footer>
<script type="module" src="/static/dashboard.js?v=5"></script>
<!-- Load public streams UI logic -->
<script type="module" src="/static/streams-ui.js?v=3"></script>
<!-- Load upload functionality -->

View File

@ -1,90 +1,43 @@
// static/magic-login.js — handles magiclink token UI
/**
* Simplified Magic Login Module
*
* This file now uses the centralized AuthManager for authentication logic.
* The token-based magic login is handled by the AuthManager.
*/
import authManager from './auth-manager.js';
import { showSection } from './nav.js';
let magicLoginSubmitted = false;
/**
* Initialize magic login - now delegated to AuthManager
* This function is kept for backward compatibility but the actual
* magic login logic is handled by the AuthManager during initialization.
*/
export async function initMagicLogin() {
console.debug('[magic-login] initMagicLogin called');
// Debug messages disabled
// The AuthManager handles both URL-based and token-based magic login
// during its initialization, so we just need to ensure it's initialized
if (!window.authManager) {
// Debug messages disabled
await authManager.initialize();
}
// Check if there was a magic login processed
const params = new URLSearchParams(location.search);
const token = params.get('token');
if (!token) {
console.debug('[magic-login] No token in URL');
return;
}
// Remove token from URL immediately to prevent loops
const url = new URL(window.location.href);
url.searchParams.delete('token');
window.history.replaceState({}, document.title, url.pathname + url.search);
try {
const formData = new FormData();
formData.append('token', token);
const res = await fetch('/magic-login', {
method: 'POST',
body: formData,
});
if (res.redirected) {
// If redirected, backend should set cookie; but set localStorage for SPA
const url = new URL(res.url);
const confirmedUid = url.searchParams.get('confirmed_uid');
if (confirmedUid) {
// Generate a simple auth token (in a real app, this would come from the server)
const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
// Set cookies and localStorage for SPA session logic
document.cookie = `uid=${encodeURIComponent(confirmedUid)}; path=/; SameSite=Lax`;
document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
// Store in localStorage for client-side access
localStorage.setItem('uid', confirmedUid);
localStorage.setItem('confirmed_uid', confirmedUid);
localStorage.setItem('authToken', authToken);
localStorage.setItem('uid_time', Date.now().toString());
}
window.location.href = res.url;
return;
}
// If not redirected, show error (shouldn't happen in normal flow)
let data;
const contentType = res.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await res.json();
if (data && data.confirmed_uid) {
// Generate a simple auth token (in a real app, this would come from the server)
const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
// Set cookies and localStorage for SPA session logic
document.cookie = `uid=${encodeURIComponent(data.confirmed_uid)}; path=/; SameSite=Lax`;
document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
// Store in localStorage for client-side access
localStorage.setItem('uid', data.confirmed_uid);
localStorage.setItem('confirmed_uid', data.confirmed_uid);
localStorage.setItem('authToken', authToken);
localStorage.setItem('uid_time', Date.now().toString());
import('./toast.js').then(({ showToast }) => {
showToast('✅ Login successful!');
// Update UI state after login
const guestDashboard = document.getElementById('guest-dashboard');
const userDashboard = document.getElementById('user-dashboard');
const registerPage = document.getElementById('register-page');
if (guestDashboard) guestDashboard.style.display = 'none';
if (userDashboard) userDashboard.style.display = 'block';
if (registerPage) registerPage.style.display = 'none';
// Show the user's stream page
if (typeof showSection === 'function') {
showSection('me-page');
}
});
return;
}
alert(data.detail || 'Login failed.');
} else {
const text = await res.text();
alert(text || 'Login failed.');
}
} catch (err) {
alert('Network error: ' + err);
if (token) {
// Debug messages disabled
} else {
// Debug messages disabled
}
}
// Export for backward compatibility
export { magicLoginSubmitted };
// Make showSection available globally for AuthManager
window.showSection = showSection;

View File

@ -1,81 +1,57 @@
import { showToast } from "./toast.js";
import { globalAudioManager } from './global-audio-manager.js';
import { SharedAudioPlayer } from './shared-audio-player.js';
// Module-level state for the personal player
let audio = null;
function getPersonalStreamUrl(uid) {
return `/audio/${encodeURIComponent(uid)}/stream.opus`;
}
function updatePlayPauseButton(button, isPlaying) {
if (button) button.textContent = isPlaying ? '⏸️' : '▶️';
// Optionally, update other UI elements here
}
const personalPlayer = new SharedAudioPlayer({
playerType: 'personal',
getStreamUrl: getPersonalStreamUrl,
onUpdateButton: updatePlayPauseButton
});
/**
* Finds or creates the audio element for the personal stream.
* @returns {HTMLAudioElement | null}
*/
function getOrCreateAudioElement() {
if (audio) {
return audio;
}
audio = document.createElement('audio');
audio.id = 'me-audio';
audio.preload = 'metadata';
audio.crossOrigin = 'use-credentials';
document.body.appendChild(audio);
// --- Setup Event Listeners (only once) ---
audio.addEventListener('error', (e) => {
console.error('Personal Player: Audio Element Error', e);
const error = audio.error;
let errorMessage = 'An unknown audio error occurred.';
if (error) {
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
errorMessage = 'Audio playback was aborted.';
break;
case error.MEDIA_ERR_NETWORK:
errorMessage = 'A network error caused the audio to fail.';
break;
case error.MEDIA_ERR_DECODE:
errorMessage = 'The audio could not be decoded.';
break;
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
errorMessage = 'The audio format is not supported by your browser.';
break;
default:
errorMessage = `An unexpected error occurred (Code: ${error.code}).`;
break;
function cleanupPersonalAudio() {
if (audioElement) {
try {
if (audioElement._eventHandlers) {
const { onPlay, onPause, onEnded, onError } = audioElement._eventHandlers;
if (onPlay) audioElement.removeEventListener('play', onPlay);
if (onPause) audioElement.removeEventListener('pause', onPause);
if (onEnded) audioElement.removeEventListener('ended', onEnded);
if (onError) audioElement.removeEventListener('error', onError);
}
audioElement.pause();
audioElement.removeAttribute('src');
audioElement.load();
if (audioElement._eventHandlers) delete audioElement._eventHandlers;
// Remove from DOM
if (audioElement.parentNode) audioElement.parentNode.removeChild(audioElement);
} catch (e) {
console.warn('[personal-player.js] Error cleaning up audio element:', e);
}
showToast(errorMessage, 'error');
});
audio.addEventListener('play', () => updatePlayPauseButton(true));
audio.addEventListener('pause', () => updatePlayPauseButton(false));
audio.addEventListener('ended', () => updatePlayPauseButton(false));
// The canplaythrough listener is removed as it violates autoplay policies.
// The user will perform a second click to play the media after it's loaded.
return audio;
}
/**
* Updates the play/pause button icon based on audio state.
* @param {boolean} isPlaying - Whether the audio is currently playing.
*/
function updatePlayPauseButton(isPlaying) {
const playPauseBtn = document.querySelector('#me-page .play-pause-btn');
if (playPauseBtn) {
playPauseBtn.textContent = isPlaying ? '⏸️' : '▶️';
audioElement = null;
}
}
/**
* Loads the user's personal audio stream into the player.
* @param {string} uid - The user's unique ID.
*/
export async function loadProfileStream(uid) {
const audioElement = getOrCreateAudioElement();
const audioSrc = `/audio/${uid}/stream.opus?t=${Date.now()}`;
console.log(`[personal-player.js] Setting personal audio source to: ${audioSrc}`);
audioElement.src = audioSrc;
// Use the shared player for loading and playing the personal stream
export function loadProfileStream(uid, playPauseBtn) {
if (!uid) {
showToast('No UID provided for profile stream', 'error');
return;
}
personalPlayer.play(uid, playPauseBtn);
}
/**
@ -91,50 +67,19 @@ export function initPersonalPlayer() {
if (!playPauseBtn) return;
e.stopPropagation();
const audio = getOrCreateAudioElement();
if (!audio) return;
try {
if (audio.paused) {
if (!audio.src || audio.src.endsWith('/#')) {
showToast('No audio file available. Please upload one first.', 'info');
return;
}
console.log('Attempting to play...');
globalAudioManager.startPlayback('personal', localStorage.getItem('uid') || 'personal');
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
console.error(`Initial play() failed: ${error.name}. This is expected on first load.`);
// If play fails, it's because the content isn't loaded.
// The recovery is to call load(). The user will need to click play again.
console.log('Calling load() to fetch media...');
audio.load();
showToast('Stream is loading. Please click play again in a moment.', 'info');
});
}
} else {
console.log('Attempting to pause...');
audio.pause();
}
} catch (err) {
console.error('A synchronous error occurred in handlePlayPause:', err);
showToast('An unexpected error occurred with the audio player.', 'error');
const uid = localStorage.getItem('uid');
if (!uid) {
showToast('Please log in to play audio.', 'error');
return;
}
// Toggle play/pause
if (personalPlayer.audioElement && !personalPlayer.audioElement.paused && !personalPlayer.audioElement.ended) {
personalPlayer.pause();
} else {
loadProfileStream(uid, playPauseBtn);
}
});
// Listen for stop requests from the global manager
globalAudioManager.addListener('personal', () => {
console.log('[personal-player.js] Received stop request from global audio manager.');
const audio = getOrCreateAudioElement();
if (audio && !audio.paused) {
console.log('[personal-player.js] Pausing personal audio player.');
audio.pause();
}
});
// Initial setup
getOrCreateAudioElement();
// Make loadProfileStream globally accessible for upload.js
window.loadProfileStream = loadProfileStream;
}

View File

@ -0,0 +1,70 @@
/**
* Cleanup Script: Remove Redundant confirmed_uid from localStorage
*
* This script removes the redundant confirmed_uid field from localStorage
* for users who might have it stored from the old authentication system.
*/
(function() {
'use strict';
console.log('[CONFIRMED_UID_CLEANUP] Starting cleanup of redundant confirmed_uid field...');
// Check if confirmed_uid exists in localStorage
const confirmedUid = localStorage.getItem('confirmed_uid');
const currentUid = localStorage.getItem('uid');
if (confirmedUid) {
console.log(`[CONFIRMED_UID_CLEANUP] Found confirmed_uid: ${confirmedUid}`);
console.log(`[CONFIRMED_UID_CLEANUP] Current uid: ${currentUid}`);
// Verify that uid exists and is properly set
if (!currentUid) {
console.warn('[CONFIRMED_UID_CLEANUP] No uid found, setting uid from confirmed_uid');
localStorage.setItem('uid', confirmedUid);
} else if (currentUid !== confirmedUid) {
console.warn(`[CONFIRMED_UID_CLEANUP] UID mismatch - uid: ${currentUid}, confirmed_uid: ${confirmedUid}`);
console.log('[CONFIRMED_UID_CLEANUP] Keeping current uid value');
}
// Remove the redundant confirmed_uid
localStorage.removeItem('confirmed_uid');
console.log('[CONFIRMED_UID_CLEANUP] Removed redundant confirmed_uid from localStorage');
// Log the cleanup action
console.log('[CONFIRMED_UID_CLEANUP] Cleanup completed successfully');
} else {
console.log('[CONFIRMED_UID_CLEANUP] No confirmed_uid found, no cleanup needed');
}
// Also check for any other potential redundant fields
const redundantFields = [
'confirmed_uid', // Main target
'confirmedUid', // Camel case variant
'confirmed-uid' // Hyphenated variant
];
let removedCount = 0;
redundantFields.forEach(field => {
if (localStorage.getItem(field)) {
localStorage.removeItem(field);
removedCount++;
console.log(`[CONFIRMED_UID_CLEANUP] Removed redundant field: ${field}`);
}
});
if (removedCount > 0) {
console.log(`[CONFIRMED_UID_CLEANUP] Removed ${removedCount} redundant authentication fields`);
}
console.log('[CONFIRMED_UID_CLEANUP] Cleanup process completed');
})();
// Export for manual execution if needed
if (typeof window !== 'undefined') {
window.removeConfirmedUidCleanup = function() {
const script = document.createElement('script');
script.src = '/static/remove-confirmed-uid.js';
document.head.appendChild(script);
};
}

View File

@ -0,0 +1,162 @@
// shared-audio-player.js
// Unified audio player logic for both streams and personal player
import { globalAudioManager } from './global-audio-manager.js';
export class SharedAudioPlayer {
constructor({ playerType, getStreamUrl, onUpdateButton }) {
this.playerType = playerType; // 'streams' or 'personal'
this.getStreamUrl = getStreamUrl; // function(uid) => url
this.onUpdateButton = onUpdateButton; // function(button, isPlaying)
this.audioElement = null;
this.currentUid = null;
this.isPlaying = false;
this.currentButton = null;
this._eventHandlers = {};
// Register stop listener
globalAudioManager.addListener(playerType, () => {
this.stop();
});
}
pause() {
if (this.audioElement && !this.audioElement.paused && !this.audioElement.ended) {
this.audioElement.pause();
this.isPlaying = false;
if (this.onUpdateButton && this.currentButton) {
this.onUpdateButton(this.currentButton, false);
}
}
}
async play(uid, button) {
const ctx = `[SharedAudioPlayer][${this.playerType}]${uid ? `[${uid}]` : ''}`;
const isSameUid = this.currentUid === uid;
const isActive = this.audioElement && !this.audioElement.paused && !this.audioElement.ended;
// Guard: If already playing the requested UID and not paused/ended, do nothing
if (isSameUid && isActive) {
if (this.onUpdateButton) this.onUpdateButton(button || this.currentButton, true);
return;
}
// If same UID but paused, resume
if (isSameUid && this.audioElement && this.audioElement.paused && !this.audioElement.ended) {
try {
await this.audioElement.play();
this.isPlaying = true;
if (this.onUpdateButton) this.onUpdateButton(button || this.currentButton, true);
globalAudioManager.startPlayback(this.playerType, uid);
} catch (err) {
this.isPlaying = false;
if (this.onUpdateButton) this.onUpdateButton(button || this.currentButton, false);
console.error(`${ctx} play() resume failed:`, err);
}
return;
}
// Otherwise, stop current and start new
if (!isSameUid && this.audioElement) {
} else {
}
this.stop();
this.currentUid = uid;
this.currentButton = button;
const url = this.getStreamUrl(uid);
this.audioElement = new Audio(url);
this.audioElement.preload = 'auto';
this.audioElement.crossOrigin = 'anonymous';
this.audioElement.style.display = 'none';
document.body.appendChild(this.audioElement);
this._attachEventHandlers();
try {
await this.audioElement.play();
this.isPlaying = true;
if (this.onUpdateButton) this.onUpdateButton(button, true);
globalAudioManager.startPlayback(this.playerType, uid);
} catch (err) {
this.isPlaying = false;
if (this.onUpdateButton) this.onUpdateButton(button, false);
console.error(`${ctx} play() failed:`, err);
}
}
stop() {
if (this.audioElement) {
this._removeEventHandlers();
try {
this.audioElement.pause();
this.audioElement.removeAttribute('src');
this.audioElement.load();
if (this.audioElement.parentNode) {
this.audioElement.parentNode.removeChild(this.audioElement);
}
} catch (e) {
console.warn('[shared-audio-player] Error cleaning up audio element:', e);
}
this.audioElement = null;
}
this.isPlaying = false;
this.currentUid = null;
if (this.currentButton && this.onUpdateButton) {
this.onUpdateButton(this.currentButton, false);
}
this.currentButton = null;
}
_attachEventHandlers() {
if (!this.audioElement) return;
const ctx = `[SharedAudioPlayer][${this.playerType}]${this.currentUid ? `[${this.currentUid}]` : ''}`;
const logEvent = (event) => {
// Debug logging disabled
};
// Core handlers
const onPlay = (e) => {
logEvent(e);
this.isPlaying = true;
if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, true);
};
const onPause = (e) => {
logEvent(e);
// console.trace(`${ctx} Audio pause stack trace:`);
this.isPlaying = false;
if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, false);
};
const onEnded = (e) => {
logEvent(e);
this.isPlaying = false;
if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, false);
};
const onError = (e) => {
logEvent(e);
this.isPlaying = false;
if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, false);
console.error(`${ctx} Audio error:`, e);
};
// Attach handlers
this.audioElement.addEventListener('play', onPlay);
this.audioElement.addEventListener('pause', onPause);
this.audioElement.addEventListener('ended', onEnded);
this.audioElement.addEventListener('error', onError);
// Attach debug logging for all relevant events
const debugEvents = [
'abort','canplay','canplaythrough','durationchange','emptied','encrypted','loadeddata','loadedmetadata',
'loadstart','playing','progress','ratechange','seeked','seeking','stalled','suspend','timeupdate','volumechange','waiting'
];
debugEvents.forEach(evt => {
this.audioElement.addEventListener(evt, logEvent);
}); // Logging now disabled
this._eventHandlers = { onPlay, onPause, onEnded, onError, debugEvents, logEvent };
}
_removeEventHandlers() {
if (!this.audioElement || !this._eventHandlers) return;
const { onPlay, onPause, onEnded, onError } = this._eventHandlers;
if (onPlay) this.audioElement.removeEventListener('play', onPlay);
if (onPause) this.audioElement.removeEventListener('pause', onPause);
if (onEnded) this.audioElement.removeEventListener('ended', onEnded);
if (onError) this.audioElement.removeEventListener('error', onError);
this._eventHandlers = {};
}
}

View File

@ -1,17 +1,30 @@
// sound.js — reusable Web Audio beep
export function playBeep(frequency = 432, duration = 0.2, type = 'sine') {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
try {
// Validate parameters to prevent audio errors
if (!Number.isFinite(frequency) || frequency <= 0) {
frequency = 432; // fallback to default
}
if (!Number.isFinite(duration) || duration <= 0) {
duration = 0.2; // fallback to default
}
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.value = frequency;
osc.type = type;
osc.frequency.value = frequency;
osc.connect(gain);
gain.connect(ctx.destination);
osc.connect(gain);
gain.connect(ctx.destination);
gain.gain.setValueAtTime(0.1, ctx.currentTime); // subtle volume
osc.start();
osc.stop(ctx.currentTime + duration);
gain.gain.setValueAtTime(0.1, ctx.currentTime); // subtle volume
osc.start();
osc.stop(ctx.currentTime + duration);
} catch (error) {
// Silently handle audio errors to prevent breaking upload flow
console.warn('[SOUND] Audio beep failed:', error.message);
}
}

View File

@ -28,7 +28,7 @@ export function initStreamsUI() {
// Register with global audio manager to handle stop requests from other players
globalAudioManager.addListener('streams', () => {
console.log('[streams-ui] Received stop request from global audio manager');
// Debug messages disabled
stopPlayback();
});
}
@ -79,10 +79,10 @@ document.addEventListener('DOMContentLoaded', () => {
function loadAndRenderStreams() {
const ul = document.getElementById('stream-list');
if (!ul) {
console.error('[STREAMS-UI] Stream list element not found');
// Debug messages disabled
return;
}
console.log('[STREAMS-UI] loadAndRenderStreams called, shouldForceRefresh:', shouldForceRefresh);
// Debug messages disabled
// Don't start a new connection if one is already active and we're not forcing a refresh
if (activeSSEConnection && !shouldForceRefresh) {
@ -140,7 +140,7 @@ function loadAndRenderStreams() {
window.location.hostname === '127.0.0.1';
if (isLocalDevelopment || window.DEBUG_STREAMS) {
const duration = Date.now() - connectionStartTime;
console.group('[streams-ui] Connection timeout reached');
// Debug messages disabled
console.log(`Duration: ${duration}ms`);
console.log('Current time:', new Date().toISOString());
console.log('Streams received:', streams.length);
@ -203,18 +203,18 @@ function loadAndRenderStreams() {
// Process the stream
function processStream({ done, value }) {
console.log('[STREAMS-UI] processStream called with done:', done);
// Debug messages disabled
if (done) {
console.log('[STREAMS-UI] Stream processing complete');
// Debug messages disabled
// Process any remaining data in the buffer
if (buffer.trim()) {
console.log('[STREAMS-UI] Processing remaining buffer data');
// Debug messages disabled
try {
const data = JSON.parse(buffer);
console.log('[STREAMS-UI] Parsed data from buffer:', data);
// Debug messages disabled
processSSEEvent(data);
} catch (e) {
console.error('[STREAMS-UI] Error parsing buffer data:', e);
// Debug messages disabled
}
}
return;
@ -237,7 +237,7 @@ function loadAndRenderStreams() {
const data = JSON.parse(dataMatch[1]);
processSSEEvent(data);
} catch (e) {
console.error('[streams-ui] Error parsing event data:', e, 'Event:', event);
// Debug messages disabled
}
}
}
@ -298,7 +298,7 @@ function loadAndRenderStreams() {
// Function to process SSE events
function processSSEEvent(data) {
console.log('[STREAMS-UI] Processing SSE event:', data);
// Debug messages disabled
if (data.end) {
if (streams.length === 0) {
ul.innerHTML = '<li>No active streams.</li>';
@ -356,7 +356,7 @@ function loadAndRenderStreams() {
// Function to handle SSE errors
function handleSSEError(error) {
console.error('[streams-ui] SSE error:', error);
// Debug messages disabled
// Only show error if we haven't already loaded any streams
if (streams.length === 0) {
@ -386,11 +386,11 @@ function loadAndRenderStreams() {
export function renderStreamList(streams) {
const ul = document.getElementById('stream-list');
if (!ul) {
console.warn('[STREAMS-UI] renderStreamList: #stream-list not found');
// Debug messages disabled
return;
}
console.log('[STREAMS-UI] Rendering stream list with', streams.length, 'streams');
console.debug('[STREAMS-UI] Streams data:', streams);
// Debug messages disabled
// Debug messages disabled
if (Array.isArray(streams)) {
if (streams.length) {
// Sort by mtime descending (most recent first)
@ -409,10 +409,10 @@ export function renderStreamList(streams) {
}
} else {
ul.innerHTML = '<li>Error: Invalid stream data.</li>';
console.error('[streams-ui] renderStreamList: streams is not an array', streams);
// Debug messages disabled
}
highlightActiveProfileLink();
console.debug('[streams-ui] renderStreamList complete');
// Debug messages disabled
}
export function highlightActiveProfileLink() {
@ -463,12 +463,7 @@ function escapeHtml(unsafe) {
.replace(/'/g, "&#039;");
}
// Function to update play/pause button state
function updatePlayPauseButton(button, isPlaying) {
if (!button) return;
button.textContent = isPlaying ? '⏸️' : '▶️';
button.setAttribute('aria-label', isPlaying ? 'Pause' : 'Play');
}
// Audio context for Web Audio API
let audioContext = null;
@ -492,7 +487,7 @@ function getAudioContext() {
// Stop current playback completely
function stopPlayback() {
console.log('[streams-ui] Stopping playback');
// Debug messages disabled
// Stop Web Audio API if active
if (audioSource) {
@ -561,120 +556,28 @@ function stopPlayback() {
currentlyPlayingAudio = null;
}
// Load and play audio using HTML5 Audio element for Opus
async function loadAndPlayAudio(uid, playPauseBtn) {
// If we already have an audio element for this UID and it's paused, just resume it
if (audioElement && currentUid === uid && audioElement.paused) {
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
return;
} catch (error) {
// Fall through to reload if resume fails
}
}
// Stop any current playback
stopPlayback();
// Notify global audio manager that streams player is starting
globalAudioManager.startPlayback('streams', uid);
// Update UI
updatePlayPauseButton(playPauseBtn, true);
currentlyPlayingButton = playPauseBtn;
currentUid = uid;
try {
// Create a new audio element with the correct MIME type
const audioUrl = `/audio/${encodeURIComponent(uid)}/stream.opus`;
// Create a new audio element with a small delay to prevent race conditions
await new Promise(resolve => setTimeout(resolve, 50));
audioElement = new Audio(audioUrl);
audioElement.preload = 'auto';
audioElement.crossOrigin = 'anonymous'; // Important for CORS
// Set up event handlers with proper binding
const onPlay = () => {
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
};
const onPause = () => {
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
};
const onEnded = () => {
isPlaying = false;
cleanupAudio();
};
const onError = (e) => {
// Ignore errors from previous audio elements that were cleaned up
if (!audioElement || audioElement.readyState === 0) {
return;
}
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
// Don't show error to user for aborted requests
if (audioElement.error && audioElement.error.code === MediaError.MEDIA_ERR_ABORTED) {
return;
}
// Show error to user for other errors
if (typeof showToast === 'function') {
showToast('Error playing audio. The format may not be supported.', 'error');
}
};
// Add event listeners
audioElement.addEventListener('play', onPlay, { once: true });
audioElement.addEventListener('pause', onPause);
audioElement.addEventListener('ended', onEnded, { once: true });
audioElement.addEventListener('error', onError);
// Store references for cleanup
audioElement._eventHandlers = { onPlay, onPause, onEnded, onError };
// Start playback with error handling
try {
const playPromise = audioElement.play();
if (playPromise !== undefined) {
await playPromise.catch(error => {
// Ignore abort errors when switching between streams
if (error.name !== 'AbortError') {
throw error;
}
});
}
isPlaying = true;
} catch (error) {
// Only log unexpected errors
if (error.name !== 'AbortError') {
console.error('[streams-ui] Error during playback:', error);
throw error;
}
}
} catch (error) {
console.error('[streams-ui] Error loading/playing audio:', error);
if (playPauseBtn) {
updatePlayPauseButton(playPauseBtn, false);
}
// Only show error if it's not an abort error
if (error.name !== 'AbortError' && typeof showToast === 'function') {
showToast('Error playing audio. Please try again.', 'error');
}
}
// --- Shared Audio Player Integration ---
import { SharedAudioPlayer } from './shared-audio-player.js';
function getStreamUrl(uid) {
return `/audio/${encodeURIComponent(uid)}/stream.opus`;
}
function updatePlayPauseButton(button, isPlaying) {
if (button) button.textContent = isPlaying ? '⏸️' : '▶️';
// Optionally, update other UI elements here
}
// Only this definition should remain; remove any other updatePlayPauseButton functions.
const streamsPlayer = new SharedAudioPlayer({
playerType: 'streams',
getStreamUrl,
onUpdateButton: updatePlayPauseButton
});
// Load and play audio using SharedAudioPlayer
function loadAndPlayAudio(uid, playPauseBtn) {
streamsPlayer.play(uid, playPauseBtn);
}
// Handle audio ended event
@ -688,7 +591,7 @@ function handleAudioEnded() {
// Clean up audio resources
function cleanupAudio() {
console.log('[streams-ui] Cleaning up audio resources');
// Debug messages disabled
// Clean up Web Audio API resources if they exist
if (audioSource) {
@ -756,32 +659,14 @@ if (streamList) {
e.preventDefault();
const uid = playPauseBtn.dataset.uid;
if (!uid) {
return;
if (!uid) return;
// Toggle play/pause using SharedAudioPlayer
if (streamsPlayer.currentUid === uid && streamsPlayer.audioElement && !streamsPlayer.audioElement.paused && !streamsPlayer.audioElement.ended) {
streamsPlayer.pause();
} else {
await loadAndPlayAudio(uid, playPauseBtn);
}
// If clicking the currently playing button, toggle pause/play
if (currentUid === uid) {
if (isPlaying) {
await audioElement.pause();
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
} else {
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
} catch (error) {
// If resume fails, try reloading the audio
await loadAndPlayAudio(uid, playPauseBtn);
}
}
return;
}
// If a different stream is playing, stop it and start the new one
stopPlayback();
await loadAndPlayAudio(uid, playPauseBtn);
});
}

View File

@ -490,7 +490,7 @@ nav#guest-dashboard.dashboard-nav {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
margin-top: 0.8em;
opacity: 0;
animation: fadeInOut 3.5s both;
animation: fadeInOut 15s both;
font-size: 1.1em;
pointer-events: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
@ -580,7 +580,7 @@ nav#guest-dashboard.dashboard-nav {
}
/* Quota meter and uploaded files section */
#quota-meter {
#uploaded-files {
background: var(--surface); /* Match article background */
border: 1px solid var(--border);
border-radius: 8px;
@ -593,19 +593,19 @@ nav#guest-dashboard.dashboard-nav {
color: var(--text-light);
}
#quota-meter {
#uploaded-files {
transition: all 0.2s ease;
}
#quota-meter h4 {
#uploaded-files h3 {
font-weight: 400;
text-align: center;
margin: 1.5rem 0 0.75rem;
margin: 0 0 27px 0;
color: var(--text);
}
#quota-meter > h4 {
margin-top: 1.5rem;
#uploaded-files > h3 {
margin: 0 0 27px 0;
text-align: center;
font-weight: 400;
color: var(--text);
@ -732,7 +732,7 @@ nav#guest-dashboard.dashboard-nav {
border-bottom: none;
}
#quota-meter:hover {
#uploaded-files:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
@ -740,7 +740,7 @@ nav#guest-dashboard.dashboard-nav {
.quota-meter {
font-size: 0.9em;
color: var(--text-muted);
margin: 0 0 1rem 0;
margin: 1rem 0 0 0;
}
#file-list {

View File

@ -14,6 +14,6 @@ export function showToast(message) {
setTimeout(() => {
toast.remove();
// Do not remove the container; let it persist for stacking
}, 3500);
}, 15000);
}

169
static/uid-validator.js Normal file
View File

@ -0,0 +1,169 @@
/**
* UID Validation Utility
*
* Provides comprehensive UID format validation and sanitization
* to ensure all UIDs are properly formatted as email addresses.
*/
export class UidValidator {
constructor() {
// RFC 5322 compliant email regex (basic validation)
this.emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// Common invalid patterns to check against
this.invalidPatterns = [
/^devuser$/i, // Legacy username pattern
/^user\d+$/i, // Generic user patterns
/^test$/i, // Test user
/^admin$/i, // Admin user
/^\d+$/, // Pure numeric
/^[a-zA-Z]+$/, // Pure alphabetic (no @ symbol)
];
}
/**
* Validate UID format - must be a valid email address
*/
isValidFormat(uid) {
if (!uid || typeof uid !== 'string') {
return {
valid: false,
error: 'UID must be a non-empty string',
code: 'INVALID_TYPE'
};
}
const trimmed = uid.trim();
if (trimmed.length === 0) {
return {
valid: false,
error: 'UID cannot be empty',
code: 'EMPTY_UID'
};
}
// Check against invalid patterns
for (const pattern of this.invalidPatterns) {
if (pattern.test(trimmed)) {
return {
valid: false,
error: `UID matches invalid pattern: ${pattern}`,
code: 'INVALID_PATTERN'
};
}
}
// Validate email format
if (!this.emailRegex.test(trimmed)) {
return {
valid: false,
error: 'UID must be a valid email address',
code: 'INVALID_EMAIL_FORMAT'
};
}
return {
valid: true,
sanitized: trimmed.toLowerCase()
};
}
/**
* Sanitize and validate UID - ensures consistent format
*/
sanitize(uid) {
const validation = this.isValidFormat(uid);
if (!validation.valid) {
console.error('[UID-VALIDATOR] Validation failed:', validation.error, { uid });
return null;
}
return validation.sanitized;
}
/**
* Validate and throw error if invalid
*/
validateOrThrow(uid, context = 'UID') {
const validation = this.isValidFormat(uid);
if (!validation.valid) {
throw new Error(`${context} validation failed: ${validation.error} (${validation.code})`);
}
return validation.sanitized;
}
/**
* Check if a UID needs migration (legacy format)
*/
needsMigration(uid) {
if (!uid || typeof uid !== 'string') {
return false;
}
const trimmed = uid.trim();
// Check if it's already a valid email
if (this.emailRegex.test(trimmed)) {
return false;
}
// Check if it matches known legacy patterns
for (const pattern of this.invalidPatterns) {
if (pattern.test(trimmed)) {
return true;
}
}
return true; // Any non-email format needs migration
}
/**
* Get validation statistics for debugging
*/
getValidationStats(uids) {
const stats = {
total: uids.length,
valid: 0,
invalid: 0,
needsMigration: 0,
errors: {}
};
uids.forEach(uid => {
const validation = this.isValidFormat(uid);
if (validation.valid) {
stats.valid++;
} else {
stats.invalid++;
const code = validation.code || 'UNKNOWN';
stats.errors[code] = (stats.errors[code] || 0) + 1;
}
if (this.needsMigration(uid)) {
stats.needsMigration++;
}
});
return stats;
}
}
// Create singleton instance
export const uidValidator = new UidValidator();
// Legacy exports for backward compatibility
export function validateUidFormat(uid) {
return uidValidator.isValidFormat(uid).valid;
}
export function sanitizeUid(uid) {
return uidValidator.sanitize(uid);
}
export function validateUidOrThrow(uid, context) {
return uidValidator.validateOrThrow(uid, context);
}

View File

@ -1,266 +1,178 @@
// upload.js — Frontend file upload handler
import { showToast } from "./toast.js";
import { playBeep } from "./sound.js";
import { logToServer } from "./logger.js";
// Initialize upload system when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// This module handles the file upload functionality, including drag-and-drop,
// progress indication, and post-upload actions like refreshing the file list.
// DOM elements are fetched once the DOM is ready
const dropzone = document.getElementById("user-upload-area");
if (dropzone) {
dropzone.setAttribute("aria-label", "Upload area. Click or drop an audio file to upload.");
}
const fileInput = document.getElementById("fileInputUser");
const fileInfo = document.createElement("div");
fileInfo.id = "file-info";
fileInfo.style.textAlign = "center";
if (fileInput) {
fileInput.parentNode.insertBefore(fileInfo, fileInput.nextSibling);
}
const streamInfo = document.getElementById("stream-info");
const streamUrlEl = document.getElementById("streamUrl");
const spinner = document.getElementById("spinner") || { style: { display: 'none' } };
let abortController;
const fileList = document.getElementById("file-list");
// Upload function
const upload = async (file) => {
if (abortController) abortController.abort();
abortController = new AbortController();
fileInfo.innerText = `📁 ${file.name}${(file.size / 1024 / 1024).toFixed(2)} MB`;
if (file.size > 100 * 1024 * 1024) {
showToast("❌ File too large. Please upload a file smaller than 100MB.");
return;
}
spinner.style.display = "block";
showToast('📡 Uploading…');
// Early exit if critical UI elements are missing
if (!dropzone || !fileInput || !fileList) {
// Debug messages disabled
return;
}
fileInput.disabled = true;
dropzone.classList.add("uploading");
const formData = new FormData();
const sessionUid = localStorage.getItem("uid");
formData.append("uid", sessionUid);
formData.append("file", file);
// Attach all event listeners
initializeUploadListeners();
const res = await fetch("/upload", {
signal: abortController.signal,
method: "POST",
body: formData,
});
let data, parseError;
try {
data = await res.json();
} catch (e) {
parseError = e;
}
if (!data) {
showToast("❌ Upload failed: " + (parseError && parseError.message ? parseError.message : "Unknown error"));
spinner.style.display = "none";
fileInput.disabled = false;
dropzone.classList.remove("uploading");
return;
}
if (res.ok) {
if (data.quota && data.quota.used_mb !== undefined) {
const bar = document.getElementById("quota-bar");
const text = document.getElementById("quota-text");
const quotaSec = document.getElementById("quota-meter");
if (bar && text && quotaSec) {
quotaSec.hidden = false;
const used = parseFloat(data.quota.used_mb);
bar.value = used;
bar.max = 100;
text.textContent = `${used.toFixed(1)} MB used`;
}
}
spinner.style.display = "none";
fileInput.disabled = false;
dropzone.classList.remove("uploading");
showToast("✅ Upload successful.");
// Refresh the audio player and file list
const uid = localStorage.getItem("uid");
if (uid) {
try {
if (window.loadProfileStream) {
await window.loadProfileStream(uid);
}
// Refresh the file list
if (window.fetchAndDisplayFiles) {
await window.fetchAndDisplayFiles(uid);
}
// Refresh the stream list to update the last update time
if (window.refreshStreamList) {
await window.refreshStreamList();
}
} catch (e) {
console.error('Failed to refresh:', e);
}
}
playBeep(432, 0.25, "sine");
} else {
if (streamInfo) streamInfo.hidden = true;
if (spinner) spinner.style.display = "none";
if ((data.detail || data.error || "").includes("music")) {
showToast("🎵 Upload rejected: singing or music detected.");
} else {
showToast(`❌ Upload failed: ${data.detail || data.error}`);
}
if (fileInput) fileInput.value = null;
if (dropzone) dropzone.classList.remove("uploading");
if (fileInput) fileInput.disabled = false;
if (streamInfo) streamInfo.classList.remove("visible", "slide-in");
}
};
// Function to fetch and display uploaded files
async function fetchAndDisplayFiles(uidFromParam) {
console.log('[UPLOAD] fetchAndDisplayFiles called with uid:', uidFromParam);
// Get the file list element
const fileList = document.getElementById('file-list');
if (!fileList) {
const errorMsg = 'File list element not found in DOM';
console.error(errorMsg);
return showErrorInUI(errorMsg);
}
// Get UID from parameter, localStorage, or cookie
const uid = uidFromParam || localStorage.getItem('uid') || getCookie('uid');
const authToken = localStorage.getItem('authToken');
const headers = {
'Accept': 'application/json',
};
// Include auth token in headers if available, but don't fail if it's not
// The server should handle both token-based and UID-based auth
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
} else {
console.debug('[UPLOAD] No auth token available, using UID-only authentication');
}
console.log('[UPLOAD] Auth state - UID:', uid, 'Token exists:', !!authToken);
/**
* Main upload function
* @param {File} file - The file to upload
*/
async function upload(file) {
// Get user ID from localStorage or cookie
const uid = localStorage.getItem('uid') || getCookie('uid');
if (!uid) {
console.error('[UPLOAD] No UID found in any source');
fileList.innerHTML = '<li class="error-message">User session expired. Please refresh the page.</li>';
// Debug messages disabled
showToast("You must be logged in to upload files.", "error");
return;
}
// Log the authentication method being used
if (!authToken) {
console.debug('[UPLOAD] No auth token found, using UID-only authentication');
} else {
console.debug('[UPLOAD] Using token-based authentication');
}
// Debug messages disabled
// Show loading state
fileList.innerHTML = '<li class="loading-message">Loading files...</li>';
// Create and display the upload status indicator
const statusDiv = createStatusIndicator(file.name);
fileList.prepend(statusDiv);
const progressBar = statusDiv.querySelector('.progress-bar');
const statusText = statusDiv.querySelector('.status-text');
const formData = new FormData();
formData.append("file", file);
formData.append("uid", uid);
try {
console.log(`[DEBUG] Fetching files for user: ${uid}`);
const response = await fetch(`/me/${uid}`, {
const response = await fetch(`/upload`, {
method: "POST",
body: formData,
headers: {
'Authorization': authToken ? `Bearer ${authToken}` : '',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
console.log('[DEBUG] Response status:', response.status, response.statusText);
if (!response.ok) {
const errorText = await response.text();
const errorMsg = `Failed to fetch files: ${response.status} ${response.statusText} - ${errorText}`;
console.error(`[ERROR] ${errorMsg}`);
throw new Error(errorMsg);
}
const data = await response.json();
console.log('[DEBUG] Received files data:', data);
if (!data.files) {
throw new Error('Invalid response format: missing files array');
}
if (data.files.length > 0) {
// Sort files by name
const sortedFiles = [...data.files].sort((a, b) => a.name.localeCompare(b.name));
fileList.innerHTML = sortedFiles.map(file => {
const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
const displayName = file.original_name || file.name;
const isRenamed = file.original_name && file.original_name !== file.name;
return `
<li class="file-item" data-filename="${file.name}">
<div class="file-name" title="${isRenamed ? `Stored as: ${file.name}` : displayName}">
${displayName}
${isRenamed ? `<div class="stored-as"><button class="delete-file" data-filename="${file.name}" data-original-name="${file.original_name}" title="Delete file">🗑️</button></div>` :
`<button class="delete-file" data-filename="${file.name}" data-original-name="${file.original_name}" title="Delete file">🗑️</button>`}
</div>
<span class="file-size">${sizeMB} MB</span>
</li>
`;
}).join('');
} else {
fileList.innerHTML = '<li class="empty-message">No files uploaded yet</li>';
}
// Delete button handling is now managed by dashboard.js
// Update quota display if available
if (data.quota !== undefined) {
const bar = document.getElementById('quota-bar');
const text = document.getElementById('quota-text');
const quotaSec = document.getElementById('quota-meter');
if (bar && text && quotaSec) {
quotaSec.hidden = false;
bar.value = data.quota;
bar.max = 100;
text.textContent = `${data.quota.toFixed(1)} MB`;
}
const errorData = await response.json().catch(() => ({ detail: 'Upload failed with non-JSON response.' }));
throw new Error(errorData.detail || 'Unknown upload error');
}
const result = await response.json();
// Debug messages disabled
playBeep(800, 0.2); // Success beep - higher frequency
// Update UI to show success
statusText.textContent = 'Success!';
progressBar.style.width = '100%';
progressBar.style.backgroundColor = 'var(--success-color)';
// Remove the status indicator after a short delay
setTimeout(() => {
statusDiv.remove();
}, 2000);
// --- Post-Upload Actions ---
await postUploadActions(uid);
} catch (error) {
const errorMessage = `Error loading file list: ${error.message || 'Unknown error'}`;
console.error('[ERROR]', errorMessage, error);
showErrorInUI(errorMessage, fileList);
}
// Helper function to show error messages in the UI
function showErrorInUI(message, targetElement = null) {
const errorHtml = `
<div style="
padding: 10px;
margin: 5px 0;
background: #2a0f0f;
border-left: 3px solid #f55;
color: var(--error-hover);
font-family: monospace;
font-size: 0.9em;
white-space: pre-wrap;
word-break: break-word;
">
<div style="font-weight: bold; color: var(--error);">Error loading files</div>
<div style="margin-top: 5px;">${message}</div>
<div style="margin-top: 10px; font-size: 0.8em; color: var(--text-muted);">
Check browser console for details
</div>
</div>
`;
if (targetElement) {
targetElement.innerHTML = errorHtml;
} else {
// If no target element, try to find it
const fileList = document.getElementById('file-list');
if (fileList) fileList.innerHTML = errorHtml;
}
// Debug messages disabled
playBeep(200, 0.5); // Error beep - lower frequency, longer duration
statusText.textContent = `Error: ${error.message}`;
progressBar.style.backgroundColor = 'var(--error-color)';
statusDiv.classList.add('upload-error');
}
}
// Helper function to get cookie value by name
/**
* Actions to perform after a successful upload.
* @param {string} uid - The user's ID
*/
async function postUploadActions(uid) {
// 1. Refresh the user's personal stream if the function is available
if (window.loadProfileStream) {
await window.loadProfileStream(uid);
}
// 2. Refresh the file list by re-fetching and then displaying.
if (window.fetchAndDisplayFiles) {
// Use email-based UID for file operations if available, fallback to uid
const fileOperationUid = localStorage.getItem('uid') || uid; // uid is now email-based
// Debug messages disabled
await window.fetchAndDisplayFiles(fileOperationUid);
}
// 3. Update quota display after upload
if (window.updateQuotaDisplay) {
const quotaUid = localStorage.getItem('uid') || uid;
// Debug messages disabled
await window.updateQuotaDisplay(quotaUid);
}
// 4. Refresh the public stream list to update the last update time
if (window.refreshStreamList) {
await window.refreshStreamList();
}
}
/**
* Creates the DOM element for the upload status indicator.
* @param {string} fileName - The name of the file being uploaded.
* @returns {HTMLElement}
*/
function createStatusIndicator(fileName) {
const statusDiv = document.createElement('div');
statusDiv.className = 'upload-status-indicator';
statusDiv.innerHTML = `
<div class="file-info">
<span class="file-name">${fileName}</span>
<span class="status-text">Uploading...</span>
</div>
<div class="progress-container">
<div class="progress-bar"></div>
</div>
`;
return statusDiv;
}
/**
* Initializes all event listeners for the upload UI.
*/
function initializeUploadListeners() {
dropzone.addEventListener("click", () => {
fileInput.click();
});
dropzone.addEventListener("dragover", (e) => {
e.preventDefault();
dropzone.classList.add("dragover");
});
dropzone.addEventListener("dragleave", () => {
dropzone.classList.remove("dragover");
});
dropzone.addEventListener("drop", (e) => {
e.preventDefault();
dropzone.classList.remove("dragover");
const file = e.dataTransfer.files[0];
if (file) {
upload(file);
}
});
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) {
upload(file);
}
});
}
/**
* Helper function to get a cookie value by name.
* @param {string} name - The name of the cookie.
* @returns {string|null}
*/
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
@ -268,35 +180,6 @@ document.addEventListener('DOMContentLoaded', () => {
return null;
}
// Export functions for use in other modules
// Make the upload function globally accessible if needed by other scripts
window.upload = upload;
window.fetchAndDisplayFiles = fetchAndDisplayFiles;
if (dropzone && fileInput) {
dropzone.addEventListener("click", () => {
console.log("[DEBUG] Dropzone clicked");
fileInput.click();
console.log("[DEBUG] fileInput.click() called");
});
dropzone.addEventListener("dragover", (e) => {
e.preventDefault();
dropzone.classList.add("dragover");
dropzone.style.transition = "background-color 0.3s ease";
});
dropzone.addEventListener("dragleave", () => {
dropzone.classList.remove("dragover");
});
dropzone.addEventListener("drop", (e) => {
dropzone.classList.add("pulse");
setTimeout(() => dropzone.classList.remove("pulse"), 400);
e.preventDefault();
dropzone.classList.remove("dragover");
const file = e.dataTransfer.files[0];
if (file) upload(file);
});
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) upload(file);
});
}
});