Files
generic-video-dl/src/routes/+layout.svelte
T
Giancarmine SalucciandClaude Opus 4.8 d89296abe5
Build and publish docker image / Build and push image to Gitea's registry (push) Successful in 41s
fix: persist login session + add proactive PWA service-worker refresh
Sessions silently dropped ~15-30 min after login on the deployed instance.
Two compounding bugs in hooks.server.ts:

- The refresh path was gated behind `if (accessToken)`, but the access
  cookie's maxAge (15m) equals the JWT TTL, so the browser deletes the
  cookie exactly when the JWT expires. Subsequent requests arrived with no
  access cookie and skipped refresh entirely -> logout. Rotation now keys
  off the `refresh` cookie (30d) instead of an expired access token.
- The old code revoked the refresh token without issuing a new `refresh`
  cookie (it assumed /api/auth/refresh handled rotation, but nothing calls
  it). Now does a full one-time-use rotation: revoke + new access + new
  refresh cookie -> sliding 30d session.

Also adopt the SvelteKit PWA proactive auto-refresh standard: the worker
already does skipWaiting()+clients.claim(); add watchSwUpdates() in the
root layout to call registration.update() on load/focus/reconnect/hourly
and reload once on controllerchange (guarded against first-install reload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:48:44 +02:00

111 lines
3.5 KiB
Svelte

<script lang="ts">
import '../app.css';
import { dev } from '$app/environment';
import { onMount } from 'svelte';
import type { LayoutData } from './$types';
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
}
let { data, children }: { data: LayoutData; children: any } = $props();
let installPrompt: BeforeInstallPromptEvent | null = $state(null);
onMount(async () => {
// register service worker
if ('serviceWorker' in navigator) {
try {
await navigator.serviceWorker.register('/service-worker.js', {
type: dev ? 'module' : 'classic'
});
} catch (e) {
console.warn('SW registration failed', e);
}
}
});
// Standard: proactive SW auto-refresh — keep installed PWAs off a stale build.
// Worker does skipWaiting()+clients.claim(); here we drive update checks
// (load/focus/reconnect/hourly) and reload once when the new worker takes over.
$effect(() => {
if (!('serviceWorker' in navigator)) return;
let refreshing = false;
// Don't reload on first install (no prior controller → claim fires controllerchange once).
const hadController = !!navigator.serviceWorker.controller;
const onControllerChange = () => {
if (refreshing || !hadController) return;
refreshing = true;
location.reload();
};
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
let reg: ServiceWorkerRegistration | undefined;
const checkForUpdate = () => void reg?.update().catch(() => {});
const onVisible = () => {
if (document.visibilityState === 'visible') checkForUpdate();
};
let timer: ReturnType<typeof setInterval> | undefined;
navigator.serviceWorker.ready.then((r) => {
reg = r;
checkForUpdate(); // on load
timer = setInterval(checkForUpdate, 60 * 60 * 1000); // hourly
});
document.addEventListener('visibilitychange', onVisible); // on focus
window.addEventListener('online', checkForUpdate); // on reconnect
return () => {
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
document.removeEventListener('visibilitychange', onVisible);
window.removeEventListener('online', checkForUpdate);
if (timer) clearInterval(timer);
};
});
// capture install prompt
$effect(() => {
const handler = (e: Event) => {
e.preventDefault();
installPrompt = e as BeforeInstallPromptEvent;
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
});
async function installApp() {
if (!installPrompt) return;
await installPrompt.prompt();
installPrompt = null;
}
</script>
<div class="min-h-screen bg-gray-950 text-gray-100">
<header class="border-b border-gray-800 bg-gray-900">
<div class="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
<a href="/" class="flex items-center gap-2 text-lg font-semibold text-white">
<span class="text-emerald-400"></span> VidDL
</a>
<nav class="flex items-center gap-4 text-sm text-gray-400">
{#if data.user}
<a href="/jobs" class="hover:text-white">Jobs</a>
<a href="/settings" class="hover:text-white">Settings</a>
{#if installPrompt}
<button onclick={installApp} class="text-emerald-400 hover:text-emerald-300">
Install app
</button>
{/if}
<form method="POST" action="/logout">
<button class="hover:text-white">Log out</button>
</form>
{/if}
</nav>
</div>
</header>
<main class="mx-auto max-w-3xl px-4 py-6">
{@render children()}
</main>
</div>