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:
@@ -439,4 +439,53 @@ describe('authStore', () => {
|
||||
uploadSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-STORE-AUTH-PERSIST-001: logout resets persisted snapshot', () => {
|
||||
it('snapshot has isAuthenticated:false after logout (PWA offline will redirect to login)', () => {
|
||||
useAuthStore.setState({ user: buildUser(), isAuthenticated: true });
|
||||
|
||||
useAuthStore.getState().logout();
|
||||
|
||||
const snapshot = JSON.parse(localStorage.getItem('trek_auth_snapshot') ?? '{}');
|
||||
expect(snapshot?.state?.isAuthenticated).toBe(false);
|
||||
expect(snapshot?.state?.user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-STORE-AUTH-PERSIST-002: 401 resets persisted snapshot', () => {
|
||||
it('snapshot has isAuthenticated:false after 401 (expired session clears offline access)', async () => {
|
||||
useAuthStore.setState({ user: buildUser(), isAuthenticated: true });
|
||||
|
||||
server.use(
|
||||
http.get('/api/auth/me', () =>
|
||||
HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
)
|
||||
);
|
||||
|
||||
await useAuthStore.getState().loadUser();
|
||||
|
||||
const snapshot = JSON.parse(localStorage.getItem('trek_auth_snapshot') ?? '{}');
|
||||
expect(snapshot?.state?.isAuthenticated).toBe(false);
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-STORE-AUTH-PERSIST-003: network error preserves snapshot', () => {
|
||||
it('snapshot retains isAuthenticated:true on network error (offline PWA skips login screen)', async () => {
|
||||
useAuthStore.setState({ user: buildUser(), isAuthenticated: true });
|
||||
|
||||
server.use(
|
||||
http.get('/api/auth/me', () =>
|
||||
HttpResponse.json({ error: 'Server error' }, { status: 500 })
|
||||
)
|
||||
);
|
||||
|
||||
await useAuthStore.getState().loadUser();
|
||||
|
||||
// Persist middleware writes the state; isAuthenticated must stay true
|
||||
const snapshot = JSON.parse(localStorage.getItem('trek_auth_snapshot') ?? '{}');
|
||||
expect(snapshot?.state?.isAuthenticated).toBe(true);
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user