mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
bc12143015
* fix(auth): recover from expired edge auth proxy sessions The service worker answered every page load from the precached app shell, so once an edge auth proxy's session cookie (e.g. Cloudflare Access, OAuth2 Proxy) expired, the proxy never saw the navigation and could not redirect to its login page. The app loaded from cache, its API calls were redirected cross-origin and failed, and it fell back to its own login screen with no way out short of clearing site data. Serve navigations network-first and fall back to the precached shell only when offline. vite-plugin-pwa defaults navigateFallback to index.html, which would register a cache-only route ahead of this one, so disable it explicitly. Cache only 200 responses so a proxy's redirect is never stored as the page. Request API redirects manually so a proxy redirect is detected directly and triggers a full reload, which the proxy can then send to its login page. Cap these reloads to one per 15 seconds so a reload answered from cache again surfaces an error instead of looping. Fixes #642 * fix(auth): harden edge auth proxy recovery against loops and cache growth Force manual redirects after caller options so no request can opt back into following a proxy redirect. Skip the reload when session storage is unavailable, since the loop guard cannot work without it. Drop the navigation network timeout so a slow proxy redirect is never replaced by the cached page, and cap the app shell cache, which stores one entry per visited URL. Ref #642 * test(auth): isolate useLoginOptions module state between tests The composable keeps its cached options, error, and in-flight request at module level, so they carried over between tests in the file. Reset the module and import it fresh for each test so a future test cannot pass or fail depending on what ran before it. Ref #642 * fix(auth): let edge proxy handle sign-in navigations Keep the offline shell under a distinct precache URL so Workbox cannot serve root navigations before the proxy sees them. Route public auth requests through the redirect-aware fetch path so expired edge sessions open reauthentication. Ref #642 * fix(auth): keep offline shell off root URLs Prevent Workbox from mapping root query variants to the precached offline shell so expired proxy sessions reach the network. --------- Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
227 lines
7.1 KiB
TypeScript
227 lines
7.1 KiB
TypeScript
import { fileURLToPath, URL } from 'node:url'
|
|
import { Agent } from 'node:http'
|
|
|
|
import { defineConfig } from 'vite'
|
|
import vue from '@vitejs/plugin-vue'
|
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import { VitePWA } from 'vite-plugin-pwa'
|
|
|
|
const apiAgent = new Agent({ keepAlive: true })
|
|
/** Lets a second dev client point at a throwaway API instance, so restart testing leaves the main stack alone. */
|
|
const apiTarget = process.env.BOOKORBIT_API_TARGET ?? 'http://localhost:6262'
|
|
const offlineShellUrl = '__bookorbit_offline_shell'
|
|
|
|
// https://vite.dev/config/
|
|
export default defineConfig({
|
|
define: {
|
|
__INTLIFY_PROD_DEVTOOLS__: false,
|
|
__VUE_I18N_FULL_INSTALL__: true,
|
|
__VUE_I18N_LEGACY_API__: false,
|
|
},
|
|
plugins: [
|
|
vue(),
|
|
vueDevTools(),
|
|
tailwindcss(),
|
|
VitePWA({
|
|
registerType: 'autoUpdate',
|
|
injectRegister: 'auto',
|
|
includeAssets: [
|
|
'favicon.ico',
|
|
'pwa-icon-source.svg',
|
|
'apple-touch-icon-180x180.png',
|
|
'pwa-64x64.png',
|
|
'pwa-192x192.png',
|
|
'pwa-512x512.png',
|
|
'maskable-icon-512x512.png',
|
|
],
|
|
manifest: {
|
|
name: 'BookOrbit',
|
|
short_name: 'BookOrbit',
|
|
description: 'Your personal book library and reading space',
|
|
theme_color: '#1e1e18',
|
|
background_color: '#fafaf8',
|
|
display: 'standalone',
|
|
start_url: '/',
|
|
scope: '/',
|
|
icons: [
|
|
{
|
|
src: 'pwa-64x64.png',
|
|
sizes: '64x64',
|
|
type: 'image/png',
|
|
},
|
|
{
|
|
src: 'pwa-192x192.png',
|
|
sizes: '192x192',
|
|
type: 'image/png',
|
|
},
|
|
{
|
|
src: 'pwa-512x512.png',
|
|
sizes: '512x512',
|
|
type: 'image/png',
|
|
},
|
|
{
|
|
src: 'maskable-icon-512x512.png',
|
|
sizes: '512x512',
|
|
type: 'image/png',
|
|
purpose: 'maskable',
|
|
},
|
|
],
|
|
shortcuts: [
|
|
{
|
|
name: 'Dashboard',
|
|
short_name: 'Dashboard',
|
|
url: '/dashboard',
|
|
icons: [{ src: 'pwa-192x192.png', sizes: '192x192' }],
|
|
},
|
|
{
|
|
name: 'Libraries',
|
|
short_name: 'Libraries',
|
|
url: '/libraries',
|
|
icons: [{ src: 'pwa-192x192.png', sizes: '192x192' }],
|
|
},
|
|
{
|
|
name: 'Settings',
|
|
short_name: 'Settings',
|
|
url: '/settings',
|
|
icons: [{ src: 'pwa-192x192.png', sizes: '192x192' }],
|
|
},
|
|
],
|
|
},
|
|
workbox: {
|
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
|
|
globIgnores: ['**/assets/foliate/**'],
|
|
// Keep the offline shell at a separate path. Workbox maps '/' and its query variants to
|
|
// precached index.html before the NetworkFirst route can contact the auth proxy.
|
|
manifestTransforms: [
|
|
(entries) => ({
|
|
manifest: entries.map((entry) => (entry.url === 'index.html' ? { ...entry, url: offlineShellUrl } : entry)),
|
|
}),
|
|
],
|
|
navigateFallback: null,
|
|
runtimeCaching: [
|
|
{
|
|
// Page loads must hit the network first: an edge auth proxy (e.g. Cloudflare Access) needs to
|
|
// see every navigation to redirect an expired session to its login page. Serving the precached
|
|
// shell unconditionally (the old `navigateFallback` behavior) hid the request from the proxy
|
|
// entirely and left the app stuck logged out with no way back in short of clearing site data.
|
|
urlPattern: ({ request, url }) => request.mode === 'navigate' && !url.pathname.startsWith('/api/'),
|
|
handler: 'NetworkFirst',
|
|
options: {
|
|
cacheName: 'app-shell',
|
|
// No networkTimeoutSeconds: a slow proxy redirect must win over the cached page.
|
|
expiration: {
|
|
maxEntries: 50,
|
|
maxAgeSeconds: 60 * 60 * 24 * 7,
|
|
},
|
|
precacheFallback: {
|
|
fallbackURL: offlineShellUrl,
|
|
},
|
|
// Status 0 here would include an auth proxy's opaqueredirect and cache it as the page.
|
|
cacheableResponse: {
|
|
statuses: [200],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
urlPattern: /^.*\/api\/v1\/books\/\d+\/cover\?(?:[^#]*&)?t=[^#]*$/,
|
|
handler: 'CacheFirst',
|
|
options: {
|
|
cacheName: 'book-covers',
|
|
expiration: {
|
|
maxEntries: 200,
|
|
maxAgeSeconds: 60 * 60 * 24 * 30,
|
|
},
|
|
cacheableResponse: {
|
|
statuses: [0, 200],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
urlPattern: /^.*\/api\/v1\/books\/\d+\/thumbnail\?(?:[^#]*&)?t=[^#]*$/,
|
|
handler: 'CacheFirst',
|
|
options: {
|
|
cacheName: 'book-thumbnails',
|
|
expiration: {
|
|
maxEntries: 200,
|
|
maxAgeSeconds: 60 * 60 * 24 * 30,
|
|
},
|
|
cacheableResponse: {
|
|
statuses: [0, 200],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
urlPattern: /^.*\/api\/v1\/books\/\d+\/cover(\?.*)?$/,
|
|
handler: 'StaleWhileRevalidate',
|
|
options: {
|
|
cacheName: 'book-covers',
|
|
expiration: {
|
|
maxEntries: 200,
|
|
maxAgeSeconds: 60 * 60 * 24 * 30,
|
|
},
|
|
cacheableResponse: {
|
|
statuses: [0, 200],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
urlPattern: /^.*\/api\/v1\/books\/\d+\/thumbnail(\?.*)?$/,
|
|
handler: 'StaleWhileRevalidate',
|
|
options: {
|
|
cacheName: 'book-thumbnails',
|
|
expiration: {
|
|
maxEntries: 200,
|
|
maxAgeSeconds: 60 * 60 * 24 * 30,
|
|
},
|
|
cacheableResponse: {
|
|
statuses: [0, 200],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
devOptions: {
|
|
enabled: false,
|
|
type: 'module',
|
|
},
|
|
}),
|
|
],
|
|
resolve: {
|
|
dedupe: ['vue'],
|
|
alias: {
|
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
'@bookorbit/types': fileURLToPath(new URL('../packages/types/src/index.ts', import.meta.url)),
|
|
},
|
|
},
|
|
optimizeDeps: {
|
|
include: ['@tanstack/vue-virtual'],
|
|
exclude: ['@embedpdf/core', '@embedpdf/core/vue'],
|
|
},
|
|
server: {
|
|
port: 6263,
|
|
strictPort: true,
|
|
host: true,
|
|
allowedHosts: true,
|
|
proxy: {
|
|
'/api': {
|
|
target: apiTarget,
|
|
agent: apiAgent,
|
|
configure: (proxy) => {
|
|
proxy.on('proxyReq', (proxyReq, req) => {
|
|
if (req.headers.host) proxyReq.setHeader('x-forwarded-host', req.headers.host)
|
|
const localPort = (req.socket as { localPort?: number })?.localPort
|
|
if (localPort) proxyReq.setHeader('x-forwarded-port', String(localPort))
|
|
proxyReq.setHeader('x-forwarded-proto', 'http')
|
|
})
|
|
},
|
|
},
|
|
'/socket.io': {
|
|
target: apiTarget,
|
|
ws: true,
|
|
changeOrigin: true,
|
|
},
|
|
},
|
|
},
|
|
})
|