fixes lol
All checks were successful
Build and Push / build-all (push) Successful in 9m12s

This commit is contained in:
doomtube 2026-01-07 02:14:34 -05:00
parent 99151c6692
commit 3676dc46ed
16 changed files with 894 additions and 89 deletions

View file

@ -1,6 +1,32 @@
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api';
async function fetchAPI(endpoint, options = {}) {
// 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,
headers: {
@ -10,6 +36,15 @@ async function fetchAPI(endpoint, options = {}) {
credentials: 'include', // Always include credentials
});
// If we get a 401 and haven't already retried, attempt token refresh
if (response.status === 401 && retryAfterRefresh) {
const refreshed = await attemptTokenRefresh();
if (refreshed) {
// Retry the original request (but don't retry again if it fails)
return fetchAPI(endpoint, options, false);
}
}
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}

View file

@ -371,7 +371,7 @@ class ChatWebSocket {
};
}
attemptReconnect() {
async attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached. Use manualReconnect() to try again.');
connectionStatus.set('failed');
@ -385,7 +385,7 @@ class ChatWebSocket {
console.log('Resetting reconnect attempts after timeout');
this.reconnectAttempts = 0;
if (this.realmId) {
this.connect(this.realmId, this.token);
this.attemptReconnect();
}
}, 60000); // Try again after 1 minute
return;
@ -400,8 +400,30 @@ class ChatWebSocket {
console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
setTimeout(async () => {
if (this.realmId) {
// If we have a token, try to refresh it before reconnecting
if (this.token) {
try {
console.log('[ChatWebSocket] Refreshing token before reconnect...');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
// Update the token with the fresh one
if (data.token) {
this.token = data.token;
console.log('[ChatWebSocket] Token refreshed successfully');
}
} else {
console.warn('[ChatWebSocket] Token refresh failed, trying with old token');
}
} catch (e) {
console.warn('[ChatWebSocket] Token refresh error:', e);
}
}
this.connect(this.realmId, this.token);
}
}, delay);
@ -410,13 +432,32 @@ class ChatWebSocket {
/**
* Manually trigger reconnection - resets attempt counter and tries immediately
*/
manualReconnect() {
async manualReconnect() {
console.log('Manual reconnect triggered');
if (this.reconnectResetTimer) {
clearTimeout(this.reconnectResetTimer);
this.reconnectResetTimer = null;
}
this.reconnectAttempts = 0;
// Refresh token before reconnecting if we have one
if (this.token && this.realmId) {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.token) {
this.token = data.token;
}
}
} catch (e) {
console.warn('Token refresh error during manual reconnect:', e);
}
}
if (this.realmId) {
this.connect(this.realmId, this.token);
}

View file

@ -347,7 +347,7 @@
{message.username.charAt(0).toUpperCase()}
</div>
{/if}
<button class="username-btn" style="color: {safeUserColor}" on:click={handleUsernameClick} on:dblclick={handleUsernameDoubleClick}>
<button class="username-btn" class:guest={message.isGuest} style="color: {message.isGuest ? 'lightgrey' : safeUserColor}" on:click={handleUsernameClick} on:dblclick={handleUsernameDoubleClick}>
{message.username}
</button>
{#if message.usedRoll}
@ -579,6 +579,12 @@
filter: invert(1);
}
.username-btn.guest {
background-color: #1f184e;
padding: 0.1rem 0.3rem;
border-radius: 2px;
}
.badge {
font-size: 0.55rem;
padding: 0.05rem 0.25rem;

View file

@ -3,6 +3,7 @@
import { watchSync, canControl, currentVideo } from '$lib/stores/watchSync';
import { auth } from '$lib/stores/auth';
import { getGuestFingerprint } from '$lib/fingerprint';
// watchSync is already imported above for sendLockUpdate
export let realmId;
export let playlist = [];
@ -295,6 +296,9 @@
item.id === itemId ? { ...item, isLocked: locked } : item
);
dispatch('playlistUpdated');
// Notify watch sync WebSocket about lock change (for immediate in-memory state update)
watchSync.sendLockUpdate(itemId, locked);
}
} catch (e) {
console.error('Failed to toggle lock:', e);

View file

@ -2,6 +2,71 @@ import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
// Token refresh interval (2 hours - before the 2.5-hour access token expires)
const REFRESH_INTERVAL_MS = 2 * 60 * 60 * 1000;
let refreshInterval = null;
let isRefreshing = false;
// Silent token refresh function
async function refreshAccessToken() {
if (isRefreshing) return false;
isRefreshing = true;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
// Update user data if returned
if (data.user) {
auth.updateUser(data.user);
}
isRefreshing = false;
return true;
} else {
// Refresh failed - session expired
console.log('Token refresh failed - session expired');
isRefreshing = false;
return false;
}
} catch (error) {
console.error('Token refresh error:', error);
isRefreshing = false;
return false;
}
}
// Start automatic token refresh
function startTokenRefresh() {
if (!browser) return;
// Clear any existing interval
if (refreshInterval) {
clearInterval(refreshInterval);
}
// Refresh every 12 minutes
refreshInterval = setInterval(async () => {
const success = await refreshAccessToken();
if (!success) {
// Stop refresh interval and logout
stopTokenRefresh();
auth.logout();
}
}, REFRESH_INTERVAL_MS);
}
// Stop automatic token refresh
function stopTokenRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
function createAuthStore() {
const { subscribe, set, update } = writable({
user: null,
@ -10,25 +75,29 @@ function createAuthStore() {
return {
subscribe,
async init() {
if (!browser) return;
// Use cookie-based auth - no localStorage tokens
try {
const response = await fetch('/api/user/me', {
credentials: 'include' // Send cookies
});
if (response.ok) {
const data = await response.json();
set({ user: data.user, loading: false });
// Start token refresh when authenticated
startTokenRefresh();
} else {
set({ user: null, loading: false });
stopTokenRefresh();
}
} catch (error) {
console.error('Auth init error:', error);
set({ user: null, loading: false });
stopTokenRefresh();
}
},
@ -46,6 +115,8 @@ function createAuthStore() {
// Server sets httpOnly cookie for HTTP requests
// Token is NOT stored in localStorage to prevent XSS attacks
set({ user: data.user, loading: false });
// Start token refresh after successful login
startTokenRefresh();
goto('/');
return { success: true };
}
@ -67,6 +138,8 @@ function createAuthStore() {
// Server sets httpOnly cookie for HTTP requests
// Token is NOT stored in localStorage to prevent XSS attacks
set({ user: data.user, loading: false });
// Start token refresh after successful login
startTokenRefresh();
goto('/');
return { success: true };
}
@ -132,20 +205,26 @@ function createAuthStore() {
},
async logout() {
// Call logout endpoint to clear httpOnly cookie
// Stop token refresh interval
stopTokenRefresh();
// Call logout endpoint to clear httpOnly cookies
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
// Clear token from localStorage
// Clear token from localStorage (legacy cleanup)
if (browser) {
localStorage.removeItem('token');
}
set({ user: null, loading: false });
goto('/login');
}
},
// Export refresh function for use by other modules (e.g., WebSocket)
refreshToken: refreshAccessToken
};
}

View file

@ -184,6 +184,15 @@ function createWatchSyncStore() {
error: data.error || 'Unknown error'
}));
break;
case 'lock_changed':
// Notify components about lock state change
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('playlist-lock-changed', {
detail: { playlistItemId: data.playlistItemId, locked: data.locked }
}));
}
break;
}
}
@ -333,6 +342,15 @@ function createWatchSyncStore() {
});
}
// Send lock update to server (for immediate in-memory state update)
function sendLockUpdate(playlistItemId, locked) {
send({
type: 'lock_update',
playlistItemId: playlistItemId,
locked: locked
});
}
// Check if local player needs to sync
function checkSync(localTime) {
let state;
@ -358,6 +376,7 @@ function createWatchSyncStore() {
requestSync,
checkSync,
reportDuration,
sendLockUpdate,
getExpectedTime: () => {
let state;
subscribe(s => { state = s; })();