bilhej/frontend/src/router/index.ts
Hermes Agent 08fcbba580
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Failing after 1m45s
CI / E2E browser tests (pull_request) Successful in 3m59s
feat(guest): guest checkout without login (Swish + QR)
Adds an anonymous guest checkout flow so a customer can order a bilhälsning
without creating an account. Payment via Swish (QR + payment link).

Backend:
- GuestOrderController: POST /api/guest-orders (public, no auth)
- CreateGuestOrderRequest / GuestOrderResponse DTOs
- Order entity: guest_email, guest_token (UUID), nullable user_id
- OrderRepository: findByGuestToken, findByGuestEmail
- OrderService: createGuestOrder, getGuestOrder by token
- SecurityConfig: /api/guest-orders/** permitAll
- V12 migration: drops user_id NOT NULL, adds guest_email + guest_token
  with partial unique index (backfill-safe for existing user orders)

Frontend:
- GuestCheckoutPage: plate lookup + order form (no login)
- GuestPaymentRedirect: Swish QR + payment link + status polling
- GuestOrderPage: order status by guest token
- guestOrders.ts API client
- router: /guest/* public routes
- vite.config: dev proxy for /api/guest-orders

Verification:
- [x] vue-tsc type-check passes (exit 0)
- [ ] Backend Java compiles (no JDK/docker in agent sandbox)
- [ ] Flyway V12 migration applies cleanly
- [ ] End-to-end POST /api/guest-orders -> 201 -> Swish -> status

Frontend type-checks but backend has NOT been compiled or run yet. This
PR is for review; backend smoke test pending in a docker environment.
2026-06-19 19:15:01 +00:00

182 lines
4.8 KiB
TypeScript

import {
createRouter,
createWebHistory,
type RouteLocationNormalized,
} from 'vue-router'
import HomePage from '@/pages/HomePage.vue'
import ComposePage from '@/pages/ComposePage.vue'
import AboutPage from '@/pages/AboutPage.vue'
import ContactPage from '@/pages/ContactPage.vue'
import PrivacyPolicyPage from '@/pages/PrivacyPolicyPage.vue'
import TermsOfServicePage from '@/pages/TermsOfServicePage.vue'
import RegisterPage from '@/pages/RegisterPage.vue'
import LoginPage from '@/pages/LoginPage.vue'
import ForgotPasswordPage from '@/pages/ForgotPasswordPage.vue'
import ResetPasswordPage from '@/pages/ResetPasswordPage.vue'
import ChangePasswordPage from '@/pages/ChangePasswordPage.vue'
import ChangeEmailPage from '@/pages/ChangeEmailPage.vue'
import ConfirmEmailChangePage from '@/pages/ConfirmEmailChangePage.vue'
import OrdersPage from '@/pages/OrdersPage.vue'
import EditOrderPage from '@/pages/EditOrderPage.vue'
import AdminPage from '@/pages/AdminPage.vue'
import PaymentRedirect from '@/pages/PaymentRedirect.vue'
import GuestCheckoutPage from '@/pages/GuestCheckoutPage.vue'
import GuestPaymentRedirect from '@/pages/GuestPaymentRedirect.vue'
import GuestOrderPage from '@/pages/GuestOrderPage.vue'
import { useAuthStore } from '@/stores/authStore'
import { getActivePinia } from 'pinia'
export function scrollBehavior(
to: RouteLocationNormalized,
_from: RouteLocationNormalized,
savedPosition: { left: number; top: number } | null,
) {
if (savedPosition) {
return savedPosition
}
if (to.hash) {
return { el: to.hash, top: 0, behavior: 'smooth' as const }
}
return { top: 0, left: 0 }
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
scrollBehavior,
routes: [
{
path: '/',
name: 'home',
component: HomePage,
},
{
path: '/compose',
name: 'compose',
component: ComposePage,
meta: { requiresAuth: true },
},
{
path: '/orders',
name: 'orders',
component: OrdersPage,
meta: { requiresAuth: true },
},
{
path: '/bestallning/:orderId/redigera',
name: 'edit-order',
component: EditOrderPage,
meta: { requiresAuth: true },
},
{
path: '/andra-losenord',
name: 'change-password',
component: ChangePasswordPage,
meta: { requiresAuth: true },
},
{
path: '/andra-epost',
name: 'change-email',
component: ChangeEmailPage,
meta: { requiresAuth: true },
},
{
path: '/admin',
name: 'admin',
component: AdminPage,
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: '/betalning/:orderId',
name: 'payment',
component: PaymentRedirect,
meta: { requiresAuth: true },
},
{
// Guest checkout — no account required to place and pay for an order.
path: '/gast-bestallning',
name: 'guest-checkout',
component: GuestCheckoutPage,
},
{
// Guest payment page — token carried in query so refresh keeps the session.
path: '/gast-betalning/:orderId',
name: 'guest-payment',
component: GuestPaymentRedirect,
},
{
// Magic-link landing — order status by opaque token.
path: '/gast-order/:token',
name: 'guest-order',
component: GuestOrderPage,
},
{
path: '/registrera',
name: 'register',
component: RegisterPage,
meta: { guestOnly: true },
},
{
path: '/logga-in',
name: 'login',
component: LoginPage,
meta: { guestOnly: true },
},
{
path: '/glomt-losenord',
name: 'forgot-password',
component: ForgotPasswordPage,
meta: { guestOnly: true },
},
{
path: '/aterstall-losenord',
name: 'reset-password',
component: ResetPasswordPage,
meta: { guestOnly: true },
},
{
path: '/bekrafta-epost',
name: 'confirm-email-change',
component: ConfirmEmailChangePage,
},
{
path: '/om-oss',
name: 'about',
component: AboutPage,
},
{
path: '/om',
redirect: '/om-oss',
},
{
path: '/kontakt',
name: 'contact',
component: ContactPage,
},
{
path: '/integritetspolicy',
name: 'privacy',
component: PrivacyPolicyPage,
},
{
path: '/villkor',
name: 'terms',
component: TermsOfServicePage,
},
],
})
router.beforeEach((to) => {
if (!getActivePinia()) return
const auth = useAuthStore()
const authenticated = auth.isAuthenticated && !auth.isTokenExpired()
if (to.meta.guestOnly && authenticated) return { name: 'home' }
if (to.meta.requiresAuth && !authenticated) {
if (auth.isAuthenticated) auth.logout()
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.meta.requiresAdmin && !auth.isAdmin) return { name: 'home' }
})
export default router