mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
**#541 — File downloads broken in PWA standalone mode**
Replace getAuthUrl + window.open pattern with blob-based fetch using
credentials:include. The old approach minted a 60s single-use ephemeral
token then called window.open, which handed the URL to the system browser
on Android/iOS — losing the PWA cookie jar and producing "invalid or
expired token". The new approach fetches the file directly inside the
PWA WebView as a blob URL, so no auth handoff occurs.
New helper client/src/utils/fileDownload.ts with downloadFile and openFile.
Updated FileManager, ReservationsPanel, ReservationModal, PlaceInspector,
CollabNotes.
Security hardening in fileDownload.ts:
- assertRelativeUrl() guard prevents credentials being sent to external hosts
- openFile() checks blob.type against a safe-inline allowlist; HTML, SVG and
other script-capable MIME types are forced to download instead of being
opened inline, preventing same-origin XSS via blob URLs
- resp.ok check covers all non-2xx responses, not just 401
**#505 — PWA offline session lost on reload**
Wrap authStore with Zustand persist middleware, serializing only
{user, isAuthenticated} to localStorage key trek_auth_snapshot.
maps_api_key is intentionally excluded from the snapshot.
On cold start with no network: persist hydrates isAuthenticated:true,
App.tsx clears isLoading and calls loadUser({silent:true}), ProtectedRoute
renders the dashboard immediately. The network error from loadUser leaves
isAuthenticated intact so no login redirect occurs.
On 401 or logout: store state is cleared, persist writes
{isAuthenticated:false} — stale snapshot does not grant offline access
after session expiry.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { authApi } from '../api/client'
|
||||
import { connect, disconnect } from '../api/websocket'
|
||||
import type { User } from '../types'
|
||||
@@ -55,7 +56,9 @@ interface AuthState {
|
||||
// Sequence counter to prevent stale loadUser responses from overwriting fresh auth state
|
||||
let authSequence = 0
|
||||
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
@@ -255,4 +258,25 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
throw new Error(error)
|
||||
}
|
||||
},
|
||||
}))
|
||||
}),
|
||||
{
|
||||
name: 'trek_auth_snapshot',
|
||||
// Only persist the minimal user snapshot needed to avoid redirecting to
|
||||
// login when the PWA reopens offline. The JWT remains in the httpOnly
|
||||
// cookie and is still validated by the server on every request.
|
||||
// maps_api_key is intentionally excluded — it's an API key that should
|
||||
// not sit in localStorage any longer than the active session requires.
|
||||
partialize: (state) => ({
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
user: state.user ? {
|
||||
id: state.user.id,
|
||||
username: state.user.username,
|
||||
email: state.user.email,
|
||||
role: state.user.role,
|
||||
avatar_url: state.user.avatar_url,
|
||||
mfa_enabled: state.user.mfa_enabled,
|
||||
must_change_password: state.user.must_change_password,
|
||||
} : null,
|
||||
}),
|
||||
}
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user