diff --git a/AUTH_GATE.md b/AUTH_GATE.md new file mode 100644 index 0000000..51464f8 --- /dev/null +++ b/AUTH_GATE.md @@ -0,0 +1,55 @@ +# Global Auth Gate + +This document explains the optional server-side login gate and what it implies for your site. + +## Overview + +- When enabled, all HTML routes require login. +- Login uses Firebase Auth (Google or email) and exchanges a Firebase ID token for a server session. +- The session is stored in a signed cookie and checked on every request. + +## Where it runs + +- The gate runs only in `vite preview` (production-like server). +- The Vite dev server (`vite dev`) does not enable the gate. +- Static hosting cannot enforce the gate, because there is no server to verify tokens or set cookies. + +## Flow + +1. User requests `/` or any HTML route. +2. Server checks the `mono_session` cookie. +3. If missing, redirect to `/login`. +4. Login page signs in with Firebase and POSTs to `/api/auth/login`. +5. Server verifies the ID token and sets a session cookie. +6. User is redirected back to `/`. + +## Configuration + +- `AUTH_ENABLED=true` enables the gate (default is false). +- `AUTH_SECRET` is required when the gate is enabled. It signs the session cookie. +- `FIREBASE_PROJECT_ID` sets the Firebase project used to verify tokens. +- `FIREBASE_CONFIG` (JSON) injects config into the login page. +- `POCKETBASE_URL` hides the custom DB setting field. +- `SESSION_MAX_AGE` sets cookie lifetime in ms (default 7 days). + +## Implications for the site + +- Requires a server runtime. Pure static hosting will not force login. +- Unauthenticated requests to non-HTML assets return 401. +- `/login` and `/login.html` remain accessible to start the flow. +- Logging out clears the session and redirects to `/login`. +- Authenticated visits to `/login` redirect back to `/`. + +## Enable (Docker) + +1. `cp .env.example .env` +2. Set `AUTH_ENABLED=true` and `AUTH_SECRET=...` +3. Optionally set `FIREBASE_CONFIG` and `FIREBASE_PROJECT_ID` +4. `docker compose up -d` +5. Visit `http://localhost:3000` + +## Enable (local preview) + +1. `npm run build` +2. Set env vars in your shell or `.env` +3. `npm run preview` diff --git a/js/app.js b/js/app.js index ae561cb..694852c 100644 --- a/js/app.js +++ b/js/app.js @@ -208,6 +208,26 @@ function hideOfflineNotification() { } } +async function disablePwaForAuthGate() { + if (!('serviceWorker' in navigator)) return; + + try { + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map((registration) => registration.unregister())); + } catch (error) { + console.warn('Failed to unregister service workers:', error); + } + + if ('caches' in window) { + try { + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys.map((key) => caches.delete(key))); + } catch (error) { + console.warn('Failed to clear caches:', error); + } + } +} + document.addEventListener('DOMContentLoaded', async () => { const api = new LosslessAPI(apiSettings); @@ -1419,14 +1439,18 @@ document.addEventListener('DOMContentLoaded', async () => { }); // PWA Update Logic - const updateSW = registerSW({ - onNeedRefresh() { - showUpdateNotification(() => updateSW(true)); - }, - onOfflineReady() { - console.log('App ready to work offline'); - }, - }); + if (window.__AUTH_GATE__) { + disablePwaForAuthGate(); + } else { + const updateSW = registerSW({ + onNeedRefresh() { + showUpdateNotification(() => updateSW(true)); + }, + onOfflineReady() { + console.log('App ready to work offline'); + }, + }); + } document.getElementById('show-shortcuts-btn')?.addEventListener('click', () => { showKeyboardShortcuts(); diff --git a/vite-plugin-auth-gate.js b/vite-plugin-auth-gate.js index d55190f..910e6cc 100644 --- a/vite-plugin-auth-gate.js +++ b/vite-plugin-auth-gate.js @@ -123,6 +123,7 @@ export default function authGatePlugin() { } if (loginHtml) { res.setHeader('Content-Type', 'text/html'); + res.setHeader('Cache-Control', 'no-store'); res.end(loginHtml); } else { res.statusCode = 404; @@ -182,6 +183,7 @@ export default function authGatePlugin() { const ext = extname(url); if ((!ext || ext === '.html') && indexHtml) { res.setHeader('Content-Type', 'text/html'); + res.setHeader('Cache-Control', 'no-store'); res.end(indexHtml); return; }