mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Fix broken service worker + device auth rejection on playlist refresh
Bug 1 (SW): Rewrote service worker fetch handler: - Skip range requests (video seeking) to avoid caching partial responses - Skip non-GET requests entirely - Use ignoreSearch on cache match to avoid query-param misses - Don't cache opaque cross-origin responses - Outer catch on Cache API failures - Don't intercept catch-all requests (let browser handle natively) - Bump cache version to v4 to purge broken cached responses Bug 2 (auth): Playlist refresh register was missing device_token, causing auth rejection every 5 minutes. Fixed by including token in the refresh-register emit. Added diagnostic logging on both client and server for token validation failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dc7450b6a7
commit
b4ac2fb821
|
|
@ -421,6 +421,7 @@
|
||||||
};
|
};
|
||||||
// Browser fingerprint (survives localStorage clear)
|
// Browser fingerprint (survives localStorage clear)
|
||||||
data.fingerprint = generateBrowserFingerprint();
|
data.fingerprint = generateBrowserFingerprint();
|
||||||
|
console.log(`[register] device_id=${data.device_id || 'none'}, has_token=${!!data.device_token}, token_len=${data.device_token?.length || 0}, paired=${config.paired}, pairing_code=${data.pairing_code || 'none'}`);
|
||||||
socket.emit('device:register', data);
|
socket.emit('device:register', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,7 +456,10 @@
|
||||||
if (refreshTimer) clearInterval(refreshTimer);
|
if (refreshTimer) clearInterval(refreshTimer);
|
||||||
refreshTimer = setInterval(() => {
|
refreshTimer = setInterval(() => {
|
||||||
if (socket?.connected && config.deviceId && config.paired) {
|
if (socket?.connected && config.deviceId && config.paired) {
|
||||||
socket.emit('device:register', { device_id: config.deviceId, device_info: {} });
|
const data = { device_id: config.deviceId, device_info: {} };
|
||||||
|
if (config.deviceToken) data.device_token = config.deviceToken;
|
||||||
|
console.log(`[refresh-register] device_id=${config.deviceId}, has_token=${!!config.deviceToken}`);
|
||||||
|
socket.emit('device:register', data);
|
||||||
}
|
}
|
||||||
}, 300000); // 5 minutes fallback
|
}, 300000); // 5 minutes fallback
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const CACHE_NAME = 'rd-player-v3';
|
const CACHE_NAME = 'rd-player-v4';
|
||||||
const CONTENT_CACHE = 'rd-content-v1';
|
const CONTENT_CACHE = 'rd-content-v1';
|
||||||
|
|
||||||
// Install: skip waiting to activate immediately
|
// Install: skip waiting to activate immediately
|
||||||
|
|
@ -19,18 +19,39 @@ self.addEventListener('activate', (event) => {
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
const url = new URL(event.request.url);
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
// Content files (videos, images): cache on first fetch for offline playback
|
// Only handle GET requests — let POST/PUT/DELETE pass through
|
||||||
|
if (event.request.method !== 'GET') return;
|
||||||
|
|
||||||
|
// Content files (videos, images): cache-first for offline playback
|
||||||
if (url.pathname.startsWith('/uploads/content/')) {
|
if (url.pathname.startsWith('/uploads/content/')) {
|
||||||
|
// Skip range requests (video seeking) — serve from network, don't cache partial responses
|
||||||
|
if (event.request.headers.get('range')) {
|
||||||
|
return; // Let the browser handle range requests directly
|
||||||
|
}
|
||||||
|
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request).then(cached => {
|
caches.open(CONTENT_CACHE).then(cache =>
|
||||||
if (cached) return cached;
|
cache.match(event.request, { ignoreSearch: true }).then(cached => {
|
||||||
return fetch(event.request).then(response => {
|
if (cached) return cached;
|
||||||
if (response.ok) {
|
return fetch(event.request).then(response => {
|
||||||
const clone = response.clone();
|
// Only cache successful, complete (non-opaque) responses
|
||||||
caches.open(CONTENT_CACHE).then(cache => cache.put(event.request, clone));
|
if (response.ok && response.status === 200 && response.type !== 'opaque') {
|
||||||
}
|
cache.put(event.request, response.clone());
|
||||||
return response;
|
}
|
||||||
}).catch(() => new Response('Offline', { status: 503 }));
|
return response;
|
||||||
|
}).catch(() => {
|
||||||
|
return new Response('Content unavailable offline', {
|
||||||
|
status: 503,
|
||||||
|
statusText: 'Service Unavailable',
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
).catch(() => {
|
||||||
|
// Cache API itself failed — fall through to network
|
||||||
|
return fetch(event.request).catch(() =>
|
||||||
|
new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/plain' } })
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -40,16 +61,24 @@ self.addEventListener('fetch', (event) => {
|
||||||
if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') {
|
if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetch(event.request).then(response => {
|
fetch(event.request).then(response => {
|
||||||
if (response.ok) {
|
if (response.ok && response.type !== 'opaque') {
|
||||||
const clone = response.clone();
|
const clone = response.clone();
|
||||||
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}).catch(() => caches.match(event.request).then(cached => cached || new Response('Offline', { status: 503 })))
|
}).catch(() =>
|
||||||
|
caches.match(event.request, { ignoreSearch: true }).then(cached =>
|
||||||
|
cached || new Response('Offline', {
|
||||||
|
status: 503,
|
||||||
|
statusText: 'Service Unavailable',
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything else: network only
|
// Everything else: network only, don't intercept failures
|
||||||
event.respondWith(fetch(event.request));
|
// (Returning without calling event.respondWith lets the browser handle it natively)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ module.exports = function setupDeviceSocket(io) {
|
||||||
if (device) {
|
if (device) {
|
||||||
// Validate device token (skip for legacy devices that don't have a token yet)
|
// Validate device token (skip for legacy devices that don't have a token yet)
|
||||||
if (device.device_token && !validateDeviceToken(device_id, device_token)) {
|
if (device.device_token && !validateDeviceToken(device_id, device_token)) {
|
||||||
console.warn(`Invalid device token for ${device_id} from ${getClientIp(socket)}`);
|
console.warn(`Invalid device token for ${device_id} from ${getClientIp(socket)} — received_len=${(device_token || '').length}, stored_len=${device.device_token.length}, received_prefix=${(device_token || '').substring(0, 8)}, stored_prefix=${device.device_token.substring(0, 8)}`);
|
||||||
socket.emit('device:auth-error', { error: 'Invalid device token' });
|
socket.emit('device:auth-error', { error: 'Invalid device token' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue