diff --git a/backend/src/controllers/RealmController.cpp b/backend/src/controllers/RealmController.cpp index bffc80e..c7a73e6 100644 --- a/backend/src/controllers/RealmController.cpp +++ b/backend/src/controllers/RealmController.cpp @@ -824,7 +824,7 @@ void RealmController::getLiveRealms(const HttpRequestPtr &, auto dbClient = app().getDbClient(); // SECURITY: Do NOT expose stream_key in public API - it allows stream hijacking *dbClient << "SELECT r.name, r.viewer_count, r.viewer_multiplier, r.offline_image_url, r.title_color, " - "u.username, u.avatar_url " + "u.username, u.avatar_url, u.user_color " "FROM realms r JOIN users u ON r.user_id = u.id " "WHERE r.is_live = true AND r.is_active = true " "ORDER BY (r.viewer_count * COALESCE(r.viewer_multiplier, 1)) DESC" @@ -850,6 +850,7 @@ void RealmController::getLiveRealms(const HttpRequestPtr &, realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as(); realm["offlineImageUrl"] = row["offline_image_url"].isNull() ? "" : row["offline_image_url"].as(); realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as(); + realm["colorCode"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as(); resp.append(realm); } diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index cb1b2f0..7e6d41b 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,31 +1,7 @@ +import { refreshAccessToken } from './stores/auth.js'; + const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api'; -// Track if we're currently refreshing to avoid multiple simultaneous refreshes -let isRefreshing = false; -let refreshPromise = null; - -// Attempt to refresh the access token -async function attemptTokenRefresh() { - if (isRefreshing) { - // Wait for the existing refresh to complete - return refreshPromise; - } - - isRefreshing = true; - refreshPromise = fetch('/api/auth/refresh', { - method: 'POST', - credentials: 'include' - }).then(response => { - isRefreshing = false; - return response.ok; - }).catch(() => { - isRefreshing = false; - return false; - }); - - return refreshPromise; -} - async function fetchAPI(endpoint, options = {}, retryAfterRefresh = true) { const response = await fetch(`${API_URL}${endpoint}`, { ...options, @@ -37,8 +13,9 @@ async function fetchAPI(endpoint, options = {}, retryAfterRefresh = true) { }); // If we get a 401 and haven't already retried, attempt token refresh + // Uses centralized refresh from auth.js (includes retry logic) if (response.status === 401 && retryAfterRefresh) { - const refreshed = await attemptTokenRefresh(); + const refreshed = await refreshAccessToken(); if (refreshed) { // Retry the original request (but don't retry again if it fails) return fetchAPI(endpoint, options, false); diff --git a/frontend/src/lib/chat/chatWebSocket.js b/frontend/src/lib/chat/chatWebSocket.js index 5ed26d8..3b55a27 100644 --- a/frontend/src/lib/chat/chatWebSocket.js +++ b/frontend/src/lib/chat/chatWebSocket.js @@ -451,8 +451,9 @@ class ChatWebSocket { /** * Manually trigger reconnection - resets attempt counter and tries immediately * Also handles authentication on existing connection (e.g., when user logs in while connected as guest) + * @param {string} [token] - Optional token to use for authentication. If not provided, will attempt to refresh. */ - async manualReconnect() { + async manualReconnect(token = null) { console.log('Manual reconnect triggered'); if (this.reconnectResetTimer) { clearTimeout(this.reconnectResetTimer); @@ -460,9 +461,25 @@ class ChatWebSocket { } this.reconnectAttempts = 0; - // Get fresh token from localStorage (may have been set by login) - const freshToken = - typeof localStorage !== 'undefined' ? localStorage.getItem('token') : null; + // Get fresh token - use provided token, or try to refresh via cookie-based auth + let freshToken = token; + if (!freshToken) { + try { + const response = await fetch('/api/auth/refresh', { + method: 'POST', + credentials: 'include' + }); + if (response.ok) { + const data = await response.json(); + if (data.token) { + freshToken = data.token; + console.log('[ChatWebSocket] Got fresh token from refresh endpoint'); + } + } + } catch (e) { + console.warn('[ChatWebSocket] Could not refresh token:', e); + } + } // If we have a token and connection is open, just send auth message // This handles the case where user logs in while already connected as guest diff --git a/frontend/src/lib/components/chat/ChatInput.svelte b/frontend/src/lib/components/chat/ChatInput.svelte index 9995735..91a4916 100644 --- a/frontend/src/lib/components/chat/ChatInput.svelte +++ b/frontend/src/lib/components/chat/ChatInput.svelte @@ -408,7 +408,6 @@ display: flex; gap: 0.5rem; padding: 0; - border-top: 1px solid var(--border); background: var(--bg-surface); flex-shrink: 0; /* Prevent input from shrinking */ } @@ -419,13 +418,12 @@ display: flex; align-items: center; background: var(--bg-input); - border: 1px solid var(--border); border-radius: 4px; overflow: visible; } .input-wrapper:focus-within { - border-color: var(--accent-blue); + outline: 1px solid var(--accent-blue); } .input-icons { diff --git a/frontend/src/lib/components/chat/ChatMessage.svelte b/frontend/src/lib/components/chat/ChatMessage.svelte index 94672e0..5e7f56e 100644 --- a/frontend/src/lib/components/chat/ChatMessage.svelte +++ b/frontend/src/lib/components/chat/ChatMessage.svelte @@ -217,10 +217,11 @@ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); } - function formatEpoch(timestamp) { - const epoch = new Date(timestamp).getTime(); - // Get last 8 digits of epoch for maximum randomness - const last8 = String(epoch).slice(-8); + function formatEpoch() { + // Use high-resolution time for microsecond precision (1000x more granular than milliseconds) + const highResTime = performance.timeOrigin + performance.now(); + const microEpoch = Math.floor(highResTime * 1000); + const last8 = String(microEpoch).slice(-8); return `No.${last8}`; } @@ -409,7 +410,7 @@ {/if} {formatTime(message.timestamp)} - {formatEpoch(message.timestamp)} + {formatEpoch()} {/if} @@ -431,7 +432,7 @@ {/if} {formatTime(message.timestamp)} - {formatEpoch(message.timestamp)} + {formatEpoch()} {/if} @@ -649,7 +650,7 @@ .epoch { color: var(--text-faint); - font-size: 0.6rem; + font-size: 0.65rem; font-family: monospace; } diff --git a/frontend/src/lib/components/chat/ChatPanel.svelte b/frontend/src/lib/components/chat/ChatPanel.svelte index 3387606..b9f41d4 100644 --- a/frontend/src/lib/components/chat/ChatPanel.svelte +++ b/frontend/src/lib/components/chat/ChatPanel.svelte @@ -719,7 +719,6 @@ max-height: 100%; min-height: 0; background: #1a1a1a; - border-left: 1px solid #333; position: relative; overflow: hidden; /* Prevent panel itself from scrolling */ } diff --git a/frontend/src/lib/components/terminal/StreamsBrowser.svelte b/frontend/src/lib/components/terminal/StreamsBrowser.svelte index 0890245..e208f4d 100644 --- a/frontend/src/lib/components/terminal/StreamsBrowser.svelte +++ b/frontend/src/lib/components/terminal/StreamsBrowser.svelte @@ -15,10 +15,10 @@ $: liveRealms = allRealms.filter(r => r.isLive); $: offlineRealms = allRealms.filter(r => !r.isLive); - // Get the current realm from URL if on a live page (e.g., /realmname/live) + // Get the current realm from URL if on a realm page (e.g., /realmname/live or /realmname/watch) $: currentLiveRealm = (() => { const path = $page.url.pathname; - const match = path.match(/^\/([^/]+)\/live$/); + const match = path.match(/^\/([^/]+)\/(live|watch)$/); return match ? match[1] : null; })(); @@ -138,10 +138,11 @@ @@ -175,10 +176,11 @@ diff --git a/frontend/src/lib/components/watch/PlaybackControls.svelte b/frontend/src/lib/components/watch/PlaybackControls.svelte index 4fe0c60..4e48659 100644 --- a/frontend/src/lib/components/watch/PlaybackControls.svelte +++ b/frontend/src/lib/components/watch/PlaybackControls.svelte @@ -1,8 +1,13 @@