fix(guest): move guest token from URL query to sessionStorage
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
This commit is contained in:
parent
48c2a50c5d
commit
120251867c
5 changed files with 36 additions and 15 deletions
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { fetchGuestOrder, type GuestOrder } from '@/api/guestOrders'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const token = route.params.token as string
|
||||
|
||||
const order = ref<GuestOrder | null>(null)
|
||||
|
|
@ -45,6 +46,18 @@ onMounted(async () => {
|
|||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function goToPayment() {
|
||||
if (!order.value) return
|
||||
// Store the guest token in sessionStorage so it does not leak via the URL
|
||||
// query string on the payment page.
|
||||
sessionStorage.setItem('guestToken', token)
|
||||
router.push({
|
||||
name: 'guest-payment',
|
||||
params: { orderId: order.value.id },
|
||||
query: { plate: order.value.plate },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -78,15 +91,7 @@ onMounted(async () => {
|
|||
</div>
|
||||
|
||||
<p v-if="order.status === 'pending_payment'" class="guest-order__hint">
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'guest-payment',
|
||||
params: { orderId: order.id },
|
||||
query: { token, plate: order.plate },
|
||||
}"
|
||||
>
|
||||
Gå till betalningssidan
|
||||
</RouterLink>
|
||||
<a href="#" @click.prevent="goToPayment"> Gå till betalningssidan </a>
|
||||
</p>
|
||||
|
||||
<div class="guest-order__letter">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ const router = useRouter()
|
|||
const route = useRoute()
|
||||
|
||||
const orderId = route.params.orderId as string
|
||||
const token = (route.query.token as string) || ''
|
||||
// Read token from sessionStorage (set by the checkout/order pages) so it
|
||||
// does not leak via the URL query string. Fall back to query.token for
|
||||
// backward compatibility with any existing magic links.
|
||||
const token =
|
||||
sessionStorage.getItem('guestToken') || (route.query.token as string) || ''
|
||||
|
||||
const plate = ref((route.query.plate as string) || '')
|
||||
const swishNumber = ref('')
|
||||
|
|
|
|||
Loading…
Reference in a new issue