Update authentication system, database models, and UI components
This commit is contained in:
@ -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
688
static/auth-manager.js
Normal 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;
|
275
static/auth.js
275
static/auth.js
@ -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
38
static/cleanup-auth.js
Normal 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 };
|
@ -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 {
|
||||
|
@ -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
220
static/file-display.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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 {
|
||||
|
@ -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 -->
|
||||
|
@ -1,90 +1,43 @@
|
||||
// static/magic-login.js — handles magic‑link 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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
70
static/remove-confirmed-uid.js
Normal file
70
static/remove-confirmed-uid.js
Normal 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);
|
||||
};
|
||||
}
|
162
static/shared-audio-player.js
Normal file
162
static/shared-audio-player.js
Normal 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 = {};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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, "'");
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
169
static/uid-validator.js
Normal 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);
|
||||
}
|
417
static/upload.js
417
static/upload.js
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user