From 120251867c2fcf7fd8de5777bfcb025470044f8b Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 18 Jul 2026 11:41:58 +0000 Subject: [PATCH] fix(guest): move guest token from URL query to sessionStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guest token (the customer's only credential) was passed as ?token=... in the URL query string on the payment page. Query strings land in browser history, nginx/reverse-proxy access logs, and can leak via Referer headers. The magic-link landing /gast-order/:token is inherently URL-based (accepted Notion/Stripe pattern), but the payment page does not need to expose the token in the URL. Changes: - GuestCheckoutPage.vue: store token in sessionStorage before navigating to payment page; remove token from query string - GuestOrderPage.vue: replace RouterLink with click handler that stores token in sessionStorage before navigating to payment; add useRouter import - GuestPaymentRedirect.vue: read token from sessionStorage with fallback to query.token for backward compatibility with existing magic links - GuestCheckoutPage.spec.ts: assert token is NOT in query, IS in sessionStorage after navigation - GuestPaymentRedirect.spec.ts: set sessionStorage in mount helper; clear sessionStorage in beforeEach sessionStorage persists across page refreshes within the same tab, so the payment page still survives refresh. If the user opens the payment URL in a new tab (from history), sessionStorage is lost — but the canonical re-entry point is the magic link /gast-order/:token, from which they can navigate to payment again. Closes #21 --- .../src/__tests__/GuestCheckoutPage.spec.ts | 3 ++- .../__tests__/GuestPaymentRedirect.spec.ts | 10 +++++++- frontend/src/pages/GuestCheckoutPage.vue | 7 ++++-- frontend/src/pages/GuestOrderPage.vue | 25 +++++++++++-------- frontend/src/pages/GuestPaymentRedirect.vue | 6 ++++- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/frontend/src/__tests__/GuestCheckoutPage.spec.ts b/frontend/src/__tests__/GuestCheckoutPage.spec.ts index 0bce7d2..bbc1b9a 100644 --- a/frontend/src/__tests__/GuestCheckoutPage.spec.ts +++ b/frontend/src/__tests__/GuestCheckoutPage.spec.ts @@ -158,8 +158,9 @@ describe('GuestCheckoutPage', () => { await vi.waitFor(() => { expect(router.currentRoute.value.name).toBe('guest-payment') expect(router.currentRoute.value.params.orderId).toBe('order-123') - expect(router.currentRoute.value.query.token).toBe('token-abc') + expect(router.currentRoute.value.query.token).toBeUndefined() expect(router.currentRoute.value.query.plate).toBe('ABC123') + expect(sessionStorage.getItem('guestToken')).toBe('token-abc') }) }) diff --git a/frontend/src/__tests__/GuestPaymentRedirect.spec.ts b/frontend/src/__tests__/GuestPaymentRedirect.spec.ts index 7e31b4b..b5f627e 100644 --- a/frontend/src/__tests__/GuestPaymentRedirect.spec.ts +++ b/frontend/src/__tests__/GuestPaymentRedirect.spec.ts @@ -67,13 +67,20 @@ async function mountPage( token = 'token-abc', plate = 'ABC123', ) { + // Store token in sessionStorage as GuestCheckoutPage and GuestOrderPage do + if (token) { + sessionStorage.setItem('guestToken', token) + } else { + sessionStorage.removeItem('guestToken') + } + const pinia = createPinia() setActivePinia(pinia) const router = createTestRouter() await router.push({ name: 'guest-payment', params: { orderId }, - query: { token, plate }, + query: { plate }, }) await router.isReady() @@ -99,6 +106,7 @@ function setupDefaultMocks() { describe('GuestPaymentRedirect', () => { beforeEach(() => { vi.clearAllMocks() + sessionStorage.clear() setupDefaultMocks() }) diff --git a/frontend/src/pages/GuestCheckoutPage.vue b/frontend/src/pages/GuestCheckoutPage.vue index 2f98efa..a089586 100644 --- a/frontend/src/pages/GuestCheckoutPage.vue +++ b/frontend/src/pages/GuestCheckoutPage.vue @@ -37,11 +37,14 @@ async function handleSubmit() { letterText.value, email.value.trim(), ) - // Token rides in the query string so the payment page survives refresh. + // Store the guest token in sessionStorage so it does not leak via the URL + // query string (browser history, access logs, Referer). sessionStorage + // persists across page refreshes within the same tab. + sessionStorage.setItem('guestToken', order.guestToken) await router.push({ name: 'guest-payment', params: { orderId: order.id }, - query: { token: order.guestToken, plate: order.plate }, + query: { plate: order.plate }, }) } catch { errorMessage.value = 'Kunde inte skapa beställningen. Försök igen senare.' diff --git a/frontend/src/pages/GuestOrderPage.vue b/frontend/src/pages/GuestOrderPage.vue index 5cbd9a4..e036fa9 100644 --- a/frontend/src/pages/GuestOrderPage.vue +++ b/frontend/src/pages/GuestOrderPage.vue @@ -1,9 +1,10 @@