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.
Previously an expired token left the frontend in a stuck state: the
router guard only checked token presence (never the exp claim), so the
user could still navigate to protected pages, and every API call then
failed with a generic Swedish "Kunde inte hämta…" message while the
header kept showing the logged-in UI. There was no global response
interceptor, and the backend returned an ambiguous 403 (no body) for
unauthenticated requests because no AuthenticationEntryPoint was
configured, making 403 mean both "no/invalid token" and "forbidden".
Backend:
- Add an AuthenticationEntryPoint in SecurityConfig that returns 401
with a Swedish {"message": ...} ErrorResponse body for
unauthenticated/expired-token requests, and an AccessDeniedHandler
returning 403 with the same body shape for genuine authorization
failures. This makes 401 = not authenticated/expired and
403 = authenticated but forbidden, the standard REST convention.
- Make JwtService(String, long) constructor public so integration
tests can mint expired tokens (was package-private).
- Update the 6 no-auth controller tests from 403 to 401
(OrderControllerTest, AdminControllerTest, PaymentControllerTest,
AuthControllerTest change-password/change-email) and assert the
message body exists; keep shouldReturn403ForNonAdminUser as 403.
- Add OrderControllerTest.shouldReturn401WithSwedishMessageWhenTokenExpired
(expired JWT via TTL -1000ms) and shouldReturn401WithMessageWhenNoAuthHeader.
Frontend:
- Add isTokenExpired() to utils/jwt.ts using the previously-unused exp
claim, and expose it on the auth store.
- Add a global 401 interceptor in api/client.ts: on a 401 from any
non-/auth/ endpoint, call auth.logout() and redirect to
/logga-in?redirect=<currentPath>. Skip /auth/ so wrong-password 401s
on login/change-password stay handled locally. Add isSessionExpired
and isForbidden helpers for per-page catch blocks.
- Harden the router guard to reject tokens whose exp is in the past
(logout + redirect to login with ?redirect=), and let expired-token
users open /logga-in and /registrera instead of bouncing to home.
- Refactor the generic-error catch blocks on OrdersPage, EditOrderPage,
ComposePage, PaymentRedirect, useAdminOrders, and useAdminOrderActions
to skip the generic Swedish message on 401 (handled globally) while
preserving wrong-password 401 handling on change-pw/email pages.
Tests:
- New frontend/src/__tests__/client.spec.ts covering 401 -> logout +
redirect, 401 from /auth/ -> no logout, 403 -> no logout, no-token
401 -> no redirect, and isSessionExpired/isForbidden helpers.
- Add authStore.spec.ts cases for isTokenExpired (no token, past exp,
future exp, missing exp, after logout).
- Add Router.spec.ts cases for expired-token redirects, token clearing,
future-exp access, and guest pages not bouncing expired users.
- Add OrdersPage.spec.ts case asserting 401 triggers no generic error
and the global logout/redirect.
- New E2E expired-token.spec.ts (Docker) covering both the router-guard
expired-token redirect and the API-401 redirect, with logged-out
header and cleared localStorage assertions.
- Mock the API in two pre-existing fake-JWT E2E tests
(auth-guards admin access, header-auth logout redirect) that broke
because the backend now correctly 401s their unsigned test-sig tokens.
Verified with ./gradlew check (frontend lint + 267 unit tests, backend
tests + coverage, Flyway, 92 E2E tests in Docker) and ./gradlew coverage;
all coverage thresholds maintained (jwt.ts at 100%).
Mobile traffic was breaking on narrow viewports because the header nav
overflowed and several pages used desktop-only spacing. This adds a
shared phone breakpoint, a hamburger menu, and scroll-to-top on route
changes so footer and menu navigation always land at the top of the page.
- Add --page-gutter and max-width 639px rules in base.css
- AppHeader: hamburger panel on small screens; flat account links on mobile
- AppFooter: stack footer links vertically on phones
- Home, compose, edit order, orders, auth, and legal pages: tighter gutters
and responsive layout (orders card actions stack; home grids single-column)
- Router scrollBehavior: scroll to top on navigation; restore on browser back
- Tests: AppHeader menu toggle, Router scrollBehavior, mobile Playwright checks
Admin page is intentionally unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the header "Byt lösenord" link with an Inställningar menu for
changing email or password. Email changes are two-step: request with
password, confirmation link to the new address, then password again on
confirm so a wrong inbox cannot take over the account.
- Backend: EmailChangeService, V10 email_change_tokens, confirm API
- Frontend: ChangeEmailPage, ConfirmEmailChangePage, header dropdown
- E2E: account-settings round-trips, Mailpit verification, wrong-password guard
- Flyway: V9 restore for dev DBs, CI migration checks, V10 for email tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace placeholder about card with hero, prose, steps, and CTA
- Add primary route /om-oss with redirect from legacy /om
- Update footer tagline and Om oss link to match new URL
- Extend AboutPage and AppFooter tests for new content and routing
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds backend endpoints and frontend edit page so pending orders can be updated or soft-cancelled without admin intervention.
Co-authored-by: Cursor <cursoragent@cursor.com>
Operators can fix prod admin passwords without email via Byt lösenord;
end users can use forgot-password when SMTP is configured. Local and CI
use Mailpit to capture outbound mail and verify reset links end-to-end.
- Backend: V8 password_reset_tokens, PasswordResetService, EmailService,
POST /api/auth/forgot-password, reset-password, change-password
- Optional testToken in forgot-password response (docker profile only, for E2E)
- Frontend: ForgotPasswordPage, ResetPasswordPage, ChangePasswordPage,
routes, login link, header Byt lösenord
- Mailpit (ghcr.io/axllent/mailpit:v1.28) in docker-compose + e2e stack
- E2E: password-reset.spec.ts + Mailpit API helper tests SMTP delivery
- Separate dev/e2e Docker image names to avoid overwriting bilhej-frontend
- Docs: README email section, production-email-checklist, .env.example
- Unit/integration tests for reset, change password, and Vitest page specs
Co-authored-by: Cursor <cursoragent@cursor.com>
- api/payment.ts: payOrder(orderId) calls POST /api/payment/{id}/pay
- api/orders.ts: add amountPaid (number|null) to Order type
- PaymentRedirect.vue: route /betalning/:orderId, shows plate from
query?plate, amount label (49 kr), green Betalt button, mock note:
"Detta är en mock-betalning. I framtiden skickas du till Stripe."
On click: calls payOrder, on success navigates to /orders, on
failure shows error. Button disables and shows "Bearbetar..." while
paying.
- ComposePage.vue: after createOrder success, captures returned order
object and navigates to /betalning/{orderId}?plate=... instead of
the old direct-to-orders route
- Router: add /betalning/:orderId route (name: payment, component:
PaymentRedirect, meta: { requiresAuth: true })
Implement client-side route protection with role-based access control. The auth
store now extracts the role claim from JWT tokens and exposes isAdmin. Router
guards enforce three levels of access: guestOnly (redirect authenticated users),
requiresAuth (redirect unauthenticated to login with redirect param), and
requiresAdmin (redirect non-admin users to home).
Changes:
- utils/jwt.ts: JWT payload parser using base64url decode (new file)
- authStore: add role ref, isAdmin computed, extractRole from JWT payload
- router: add route metadata (requiresAuth, requiresAdmin, guestOnly) and
beforeEach guard with getActivePinia() safety for test environments
- OrdersPage.vue, AdminPage.vue: placeholder pages (new files)
- LoginPage.vue, RegisterPage.vue: use route.query.redirect after auth
- Router.spec.ts: 14 tests covering all guard scenarios
- authStore.spec.ts: tests for role extraction, isAdmin, role persistence
- LoginPage.spec.ts: test for redirect query param after login
- auth-guards.spec.ts: 7 Playwright E2E tests for guard behavior
- login.spec.ts: fix seed user credentials (test@bilhalsning.se)
Add the frontend login page (LoginPage.vue) with email and password
fields, Swedish UI strings, and integration with the backend login
endpoint. Also sets up Playwright as the E2E testing framework with
browser tests for both login and registration flows.
Frontend login implementation:
- Add LoginPage.vue with form validation, error handling, and link to
registration page
- Add login() API function in auth.ts
- Add loginUser() method to authStore that stores JWT token
- Add /logga-in route to Vue Router
- Add 'Logga in' nav link to AppHeader alongside existing 'Registrera'
- Add 10 unit tests for LoginPage component
- Add 4 unit tests for loginUser auth store method
- Add 1 route resolution test and 1 AppHeader link test
Playwright E2E setup and tests:
- Install @playwright/test and configure playwright.config.ts
- Add npm scripts: test:e2e (local) and test:e2e:ci (Docker CI)
- Exclude e2e/ directory from Vitest to prevent test runner conflicts
- Add .gitignore entries for test-results/ and playwright-report/
- Add 5 E2E tests for login (navigation, invalid credentials, success
redirect, navigation to register, input types)
- Add 6 E2E tests for register (navigation, success redirect, validation
errors for invalid email/short password/mismatched passwords,
navigation to login)
Add AppHeader and AppFooter to give the site a consistent chrome
around the core page content. Add ComposePage stub reachable via
"Skicka ett brev till ägaren" CTA on HomePage after vehicle lookup
succeeds. Add stub pages for about, contact, and privacy.
- Create AppHeader.vue with logo link (BilHälsning) and Hem nav link
- Create AppFooter.vue with 4 links: Om oss, Kontakt, Integritetspolicy, Villkor
- Create ComposePage.vue stub that reads plate from route query params
- Create AboutPage.vue and ContactPage.vue stub pages
- Add 4 new routes: /compose, /om, /kontakt, /integritetspolicy
- Update App.vue to render AppHeader + <main> + AppFooter around RouterView
- Add home__cta RouterLink button to HomePage, visible only when vehicle
lookup succeeds, linking to /compose?plate=<plate>
- Remove BilHälsning h1 from HomePage (moved to header)
- Add 17 new tests: AppHeader (2), AppFooter (1), ComposePage (3),
AboutPage (1), ContactPage (1), HomePage rewrite (6), App update (2)
- Update App.spec.ts to verify header/footer components render