Domain=.sami cookies are silently rejected by real browsers - .sami is an unregistered custom TLD, so browsers treat sami itself as the effective public suffix and refuse to set a cookie scoped to it (the same rule that stops a site from setting a supercookie for all of .com). Confirmed via curl verbose (cookie dropped, domain must not set cookies for sami) and via the Firefox console on the actual device (Cookie rejected for invalid domain) for the same cookie. The session cookie set on status.sami after TOTP verify could never reach plex.sami/jellyfin.sami/emby.sami/chat.sami no matter how the cookie itself was built - prior fixes tonight left this mechanism untouched, which is why the loop persisted. Fix: /totp/verify mints a short-lived (60s) single-use opaque token. The status.sami frontend appends it to the redirect URL when bouncing the user back to a gated service. That services login page exchanges the token via the new public GET /api/v1/auth/sso-exchange for a host-only session cookie (no Domain attribute - always accepted). isSessionValid only checks the cookies HMAC signature, never its Domain, so the host-only cookie validates identically to the cross-domain one on every existing check with zero changes to that logic.
119 lines
3.5 KiB
JavaScript
119 lines
3.5 KiB
JavaScript
const CACHE = 'dashcaddy-shell-4706f2a8fa';
|
|
const PRECACHE = [
|
|
'/',
|
|
'/index.html',
|
|
'/css/themes.css',
|
|
'/css/dashboard.css',
|
|
'/css/driver.min.css',
|
|
'/css/onboarding.css',
|
|
'/dist/core.js',
|
|
'/dist/features.js',
|
|
'/dist/init.js',
|
|
'/dist/onboarding.js',
|
|
'/assets/fonts.css',
|
|
'/assets/site.webmanifest',
|
|
'/assets/favicon.svg',
|
|
'/assets/dashcaddy-favicon.ico',
|
|
'/assets/icon-192.png',
|
|
'/assets/icon-512.png',
|
|
'/assets/apple-touch-icon.png',
|
|
'/assets/dashcaddy-logo-dark.png',
|
|
'/assets/dashcaddy-logo-light.png',
|
|
'/assets/sami7777-logo.png',
|
|
'/assets/fonts/sami-grotesk/SamiGrotesk-Regular.woff2',
|
|
'/assets/fonts/sami-grotesk/SamiGrotesk-Medium.woff2',
|
|
'/assets/fonts/sami-grotesk/SamiGrotesk-Bold.woff2',
|
|
'/assets/fonts/DSEG7Classic-Bold.woff2',
|
|
'/assets/weather/clear-day.svg',
|
|
'/assets/weather/clear-night.svg',
|
|
'/assets/weather/partly-cloudy-day.svg',
|
|
'/assets/weather/partly-cloudy-night.svg',
|
|
'/assets/weather/cloudy.svg',
|
|
'/assets/weather/fog.svg',
|
|
'/assets/weather/drizzle.svg',
|
|
'/assets/weather/rain.svg',
|
|
'/assets/weather/sleet.svg',
|
|
'/assets/weather/snow.svg',
|
|
'/assets/weather/thunderstorm.svg',
|
|
'/assets/weather/wind.svg'
|
|
];
|
|
|
|
function isNavigationRequest(request) {
|
|
return request.mode === 'navigate';
|
|
}
|
|
|
|
function isStaticAsset(pathname) {
|
|
return pathname.startsWith('/assets/')
|
|
|| pathname.startsWith('/css/')
|
|
|| pathname.startsWith('/dist/');
|
|
}
|
|
|
|
async function networkFirst(request, preloadResponsePromise) {
|
|
const cache = await caches.open(CACHE);
|
|
try {
|
|
const preloadResponse = preloadResponsePromise ? await preloadResponsePromise : null;
|
|
if (preloadResponse) {
|
|
cache.put(request, preloadResponse.clone()).catch(() => {});
|
|
return preloadResponse;
|
|
}
|
|
const response = await fetch(request);
|
|
cache.put(request, response.clone()).catch(() => {});
|
|
return response;
|
|
} catch (_) {
|
|
return caches.match(request) || caches.match('/index.html');
|
|
}
|
|
}
|
|
|
|
async function staleWhileRevalidate(request) {
|
|
const cache = await caches.open(CACHE);
|
|
const cached = await cache.match(request);
|
|
|
|
const networkPromise = fetch(request)
|
|
.then((response) => {
|
|
cache.put(request, response.clone()).catch(() => {});
|
|
return response;
|
|
})
|
|
.catch(() => null);
|
|
|
|
if (cached) return cached;
|
|
return networkPromise.then((response) => response || Response.error());
|
|
}
|
|
|
|
self.addEventListener('install', (event) => {
|
|
self.skipWaiting();
|
|
event.waitUntil(
|
|
caches.open(CACHE).then((cache) =>
|
|
cache.addAll(PRECACHE.map((url) => new Request(url, { cache: 'reload' })))
|
|
)
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil((async () => {
|
|
const keys = await caches.keys();
|
|
await Promise.all(keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)));
|
|
if ('navigationPreload' in self.registration) {
|
|
await self.registration.navigationPreload.enable();
|
|
}
|
|
await self.clients.claim();
|
|
})());
|
|
});
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event;
|
|
if (request.method !== 'GET') return;
|
|
|
|
const url = new URL(request.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
if (url.pathname.startsWith('/api/v1/') || url.pathname.startsWith('/probe/')) return;
|
|
|
|
if (isNavigationRequest(request) || url.pathname === '/' || url.pathname.endsWith('/index.html')) {
|
|
event.respondWith(networkFirst(request, event.preloadResponse));
|
|
return;
|
|
}
|
|
|
|
if (isStaticAsset(url.pathname)) {
|
|
event.respondWith(staleWhileRevalidate(request));
|
|
}
|
|
});
|