Commit graph

45 commits

Author SHA1 Message Date
81e3968e31 Log out users automatically when their JWT expires.
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m11s
CI / E2E browser tests (pull_request) Successful in 3m57s
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%).
2026-06-17 12:43:31 +02:00
c7eeaf6a6b Refactor admin fulfillment into focused modules.
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m9s
CI / E2E browser tests (pull_request) Successful in 4m1s
Extract AdminOrderWorkflowService and status rules API; split AdminPage
into composables and components; share order status constants; update tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 14:34:03 +02:00
5b5b44194d Fix V7 dev seed for H2 compatibility.
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Failing after 2m15s
CI / E2E browser tests (pull_request) Failing after 1m34s
Remove PostgreSQL-only ON CONFLICT so Flyway succeeds in tests and local H2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 12:40:22 +02:00
1c9269699e Add admin order fulfillment tracking.
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Failing after 1m50s
CI / E2E browser tests (pull_request) Failing after 1m38s
Register PostNord shipments, admin notes, and guarded status transitions
with customer emails. Expandable admin UI, V11 migration, serial E2E suite,
and AGENTS.md Docker-only E2E guidance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 12:21:17 +02:00
b2aaeb5733 Merge origin/master into feature/account-settings-dropdown.
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m24s
CI / E2E browser tests (pull_request) Successful in 1m31s
Resolve router conflict: keep /bekrafta-epost confirm route alongside
master's /om-oss about page and /om redirect.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 14:34:38 +02:00
3532e4d486 Add account settings dropdown and verified email change flow.
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m9s
CI / E2E browser tests (pull_request) Successful in 1m55s
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>
2026-05-22 14:33:06 +02:00
15d7b4ae4c Fix order cancellation by allowing cancelled in the database status constraint.
All checks were successful
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 2m26s
CI / E2E browser tests (pull_request) Successful in 1m18s
The cancel API returned 500 because ck_orders_status did not include cancelled.
Adds Flyway V9 and an E2E test for cancelling a pending order from /orders.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 11:53:25 +02:00
3d0b7fe799 Allow users to edit or cancel unpaid orders before payment.
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>
2026-05-22 11:21:47 +02:00
de86880a8f Fix prod SMTP STARTTLS for Resend password-reset mail.
Some checks failed
CI / E2E browser tests (push) Has been cancelled
CI / Lint, type check, unit tests, coverage (push) Has been cancelled
The docker profile disables mail.smtp.starttls for Mailpit; prod runs
docker+prod so Resend saw AUTH before STARTTLS (538). Re-enable auth and
STARTTLS in application-prod.yml.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 18:50:18 +02:00
ad195fd890 Wire production email secrets through Forgejo deploy.
All checks were successful
CI / Lint, type check, unit tests, coverage (push) Successful in 2m2s
CI / E2E browser tests (push) Successful in 1m16s
Deploy workflow now writes MAIL_* and APP_PUBLIC_BASE_URL from Actions
secrets into the server .env so Resend SMTP works after domain verify.
Document Resend-only setup, Forgejo secret names, and prod expose-token off.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 18:39:00 +02:00
86fb946e33 Add password reset, logged-in change password, and Mailpit email dev/E2E.
All checks were successful
CI / Lint, type check, unit tests, coverage (push) Successful in 2m2s
CI / E2E browser tests (push) Successful in 1m55s
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>
2026-05-21 18:05:15 +02:00
fb9713d8d8 Fix prod deploy Flyway validation for removed dev seed migrations.
Some checks failed
CI / E2E browser tests (push) Has been cancelled
CI / Lint, type check, unit tests, coverage (push) Has been cancelled
Backend crashed on startup because the prod DB still records V6 (and
possibly V2/V4) from when seeds lived in db/migration, while prod only
loads schema migrations. ignore-migration-patterns alone did not prevent
validate failure on the runner.

- Run Flyway repair before migrate on the prod profile
- Add ProdFlywayConfigTest for repair-then-migrate order
- Document the V6 error in README deploy troubleshooting
2026-05-21 16:52:15 +02:00
93ece8128a Use bilhej.se domain for dev test user email.
Aligns seeded and test login addresses with production branding while
keeping admin@bilhalsning.se for local docker admin seed only.

- Change test@bilhalsning.se to test@bilhej.se in dev migration and all tests
2026-05-21 15:14:11 +02:00
75911dfffa Separate dev database seeds from production and bootstrap prod admin.
Production must not ship test users, demo orders, or test1234. Dev and CI
still need seeded users for e2e. Prod creates one admin from deploy secrets.

- Move V2/V4/V6 seed migrations to db/dev-migration
- Add application-prod.yml with schema-only Flyway and ignore-missing for moved seeds
- Add AdminBootstrap to create admin from ADMIN_EMAIL and ADMIN_PASSWORD
- Wire docker,prod profile, deploy secrets, and localhost:5433 for SSH DB access
- Add hashPassword Gradle task for optional manual bcrypt generation
2026-05-21 15:14:03 +02:00
4385f43b08 Add application-prod.yml for production Flyway config 2026-05-21 15:13:56 +02:00
e2bccb4029 Include letter text in user-facing order API responses.
Users need the full message on Mina beställningar after creating an
order. The admin API already exposed letterText; user list and payment
confirm endpoints did not.

- Add letterText field to OrderResponse DTO
- Map letterText in OrderController.toResponse and PaymentController.toResponse
- Assert letterText in OrderControllerTest list and create expectations
2026-05-21 14:49:50 +02:00
98d5545be0 feat: replace Stripe mock with manual Swish payment flow
Replace the mock test-payment button with a real manual Swish flow
where the user sends a Swish payment with the order ID as message
and confirms via a button. Admin verifies Swish and processes manually.

Backend
- Rename OrderStatus LOOKUP_STARTED to PROCESSING (Swedish: Hanteras)
- Update V5 migration CHECK constraint from lookup_started to processing
- Rename OrderService.markAsPaid() to confirmPayment(), sets PROCESSING
  instead of PAID, stop hardcoding amountPaid
- Add GET /api/payment/swish-info endpoint returning swish number and
  letter price from app.payment config
- Permit /api/payment/swish-info without authentication
- Update UpdateStatusRequest regex to accept processing
- Update PaymentControllerTest for renamed method, new status, and
  public swish-info endpoint test

Frontend
- Rewrite PaymentRedirect.vue: Swish number, order ID as message,
  Jag har betalat button with confirmation dialog
- Add fetchSwishInfo() to api/payment.ts
- AdminPage: rename Skickade stat to Att göra (processing orders),
  highlight processing rows with admin__row--todo
- OrdersPage: update status labels/badge classes for new flow
- Refactor ApiError in client.ts to property declaration syntax
- Exclude __tests__ from tsconfig.app.json and Docker builds

Tests
- Rewrite PaymentRedirect.spec.ts for Swish info, confirmation dialog,
  cancel flow, and processing status
- Update OrdersPage.spec.ts with processing status test
- Update AdminDashboard.spec.ts with Att göra stat and row highlight
- Add amountPaid to ComposePage.spec.ts mock

Config
- Add SWISH_NUMBER to .env.example and docker-compose.yml
2026-05-19 19:23:37 +02:00
3792fdec82 test: add service and controller tests for vehicle lookup
- Add real HTML fixture from biluppgifter.se/fordon/hdo732/ containing:
  summary cards (.info > em + span) for Modellar, Typ, Farg, Bransle
  Fordonsdata section (ul.list with span.label/span.value) for Fabrikat, Modell, Variant, Fordonsar/Modellar
- Add VehicleLookupServiceTest with 6 cases:
  shouldParseAllFieldsFromFixture, shouldParseSummaryFields,
  shouldParseDataSectionFields, shouldReturnEmptyFieldsForEmptyDocument,
  shouldBuildModelWithoutVariant, shouldFallbackToModellarWhenNoFordonsar
- Add VehicleControllerTest with 4 cases:
  shouldReturnVehicleInfoForValidPlate (200 with all fields),
  shouldReturn404WhenVehicleNotFound, shouldBeAccessibleWithoutAuthentication,
  shouldReturnVehicleInfoWithFuelField
2026-05-19 15:15:50 +02:00
18f462c5c1 feat: add real vehicle lookup via biluppgifter.se scraping
- Add VehicleInfoResponse DTO record with make, model, year, color, fuel fields
- Add VehicleNotFoundException for unknown plates (returns 404)
- Add VehicleLookupException for scrape failures (returns 500)
- Add handlers in GlobalExceptionHandler: 404 'Inget fordon hittades', 500 'Ett internt fel uppstod'
- Add VehicleLookupService that fetches biluppgifter.se/fordon/{plate}/ HTML
- Parse summary cards (.info > em + span) for Farg, Bransle, Modellar
- Parse Fordonsdata section (ul.list > li with span.label / span.value) for Fabrikat, Modell, Variant, Fordonsar
- Build model from Modell + Variant, parse year from Fordonsar / Modellar with Modellar fallback
- Filter out 'Logga in' placeholder values from gated fields
- Add VehicleController with GET /api/vehicles/{plate}, public endpoint (already permitAll)
2026-05-19 15:15:20 +02:00
6dc9b6de33 feat: add Jsoup 1.18.1 dependency for HTML parsing
- Add org.jsoup:jsoup:1.18.1 to backend dependencies
- Will be used by VehicleLookupService to scrape vehicle data from biluppgifter.se
2026-05-19 15:11:01 +02:00
d27bde2fbe test: add PaymentControllerTest with 4 cases
- shouldReturn403WhenNotAuthenticated: verifies the endpoint requires
  a valid JWT token (anyRequest().authenticated() enforcement)
- shouldMarkOrderAsPaidSuccessfully: calls POST with @WithMockUser,
  verifies response includes id, status=paid, and amountPaid=49.00
- shouldReturn404WhenOrderNotFound: mocks service to throw
  OrderNotFoundException, expects 404 response
- Test helper creates minimal Order entity with explicitly set id,
  plate, status, and amountPaid for realistic response mapping
2026-05-15 20:30:02 +02:00
744ff00b9d feat: add POST /api/payment/{orderId}/pay mock payment endpoint
- PaymentController: @RestController at /api/payment, requires
  authentication (covered by SecurityConfig.anyRequest().authenticated())
- POST /{orderId}/pay: calls orderService.markAsPaid(orderId) which
  sets status=PAID and amountPaid=49.00, returns updated OrderResponse
- No Stripe integration yet — pure mock simulating what a successful
  Stripe webhook callback would do in Phase 1
- toResponse() mapper reuses the same OrderResponse structure as
  OrderController for consistent API shape
2026-05-15 20:29:42 +02:00
00ada956bf refactor: add amountPaid to OrderResponse and markAsPaid to OrderService
- OrderResponse record: add BigDecimal amountPaid field — null means
  the order hasn't been paid yet; 49.00 when paid via payment page
- OrderService.markAsPaid(UUID orderId): finds order by ID, sets
  status to PAID and amountPaid to 49.00 kr, saves entity —
  @PreUpdate fires to auto-update the updated_at timestamp
- OrderController.toResponse() mapper updated to include
  order.getAmountPaid() in the response DTO
- Existing controller and service tests pass unchanged — the new
  field in the record adds a default null parameter to existing
  constructor calls without breaking
2026-05-15 20:29:31 +02:00
ebab892e93 feat: add PATCH /api/admin/orders/{id} for manual tracking entry
- UpdateTrackingRequest DTO: optional trackingId string (nullable —
  allows clearing a tracking ID entered incorrectly)
- OrderService.updateTracking(orderId, trackingId): finds order,
  sets trackingId via setter, saves entity — @PreUpdate fires to
  update the updated_at timestamp automatically
- AdminController.PATCH /api/admin/orders/{id}: admin-only endpoint,
  validates request body with @Valid, returns updated AdminOrderResponse
  via the existing toAdminResponse() mapper
- AdminControllerTest: 5 new tests —
  shouldReturn403WhenPatchingTrackingWithoutAuth,
  shouldReturn403WhenPatchingTrackingAsNonAdmin,
  shouldUpdateTrackingSuccessfully (verifies response id and trackingId),
  shouldClearTrackingWhenNull (removes trackingId),
  shouldReturn404WhenOrderNotFoundForTracking
2026-05-15 19:58:33 +02:00
f6825ec885 test: add OrderStatusConverter and SubscriptionConverter unit tests
- OrderStatusConverterTest (6 tests): null-to-null, value-to-string,
  string-to-enum matching, null-to-null reverse, invalid string throws
  IllegalArgumentException, roundtrip all 6 OrderStatus values
- SubscriptionConverterTest (6 tests): same pattern for 3 subscription
  values (NONE/BASIC/PRO)
- Pure unit tests — no Spring context, no database
- Raises backend branch coverage from 45.5% to 77.3% (both converters
  now at 100% branch and line coverage)
- Unblocks ./gradlew check: the 60% branch threshold was previously
  failing due to untested converter logic
2026-05-15 19:58:18 +02:00
fc5e9ddda7 chore: add JaCoCo coverage enforcement to backend
- Add jacoco plugin (bundled with Gradle, no extra dependency)
- jacocoTestReport: generates HTML + XML reports, runs after test
- jacocoTestCoverageVerification: enforces 70% line coverage and
  60% branch coverage at the bundle level
- Wire jacocoTestCoverageVerification into tasks.named('check') so
  ./gradlew check blocks if coverage drops below thresholds
- HTML report output: backend/build/reports/jacoco/index.html
- test task finalizedBy jacocoTestReport so report is always
  available after running tests
2026-05-15 12:15:45 +02:00
5df7c97977 test: add AdminControllerTest with 10 role-enforcement and validation cases
- GET /api/admin/orders:
  - shouldReturn403WhenNotAuthenticated
  - shouldReturn403ForNonAdminUser (roles = USER)
  - shouldReturnAllOrdersForAdmin (roles = ADMIN, checks all response fields
    including email, plate, letterText, status)
  - shouldReturnEmptyArrayWhenNoOrders
- PATCH /api/admin/orders/{id}/status:
  - shouldReturn403WhenPatchingStatusWithoutAuth
  - shouldReturn403WhenPatchingStatusAsNonAdmin
  - shouldUpdateOrderStatusSuccessfully (verifies response id matches
    path variable, status reflects update)
  - shouldReturn400WhenStatusIsInvalid (invalid_status rejected by
    @Pattern validator)
  - shouldReturn400WhenStatusIsBlank
  - shouldReturn404WhenOrderNotFound
- Helper createOrder(UUID orderId, String plate, String email,
  OrderStatus) builds domain objects with User relationship for
  realistic response mapping
2026-05-15 12:15:06 +02:00
76028fa94d feat: add GET /api/admin/orders and PATCH /api/admin/orders/{id}/status
- AdminOrderResponse DTO: extends OrderResponse with email (from User
  relation) and letterText fields, exposing the full order for admin review
- UpdateStatusRequest DTO: single "status" field validated against all
  six OrderStatus values (pending_payment|paid|lookup_started|sent|
  delivered|failed) with Swedish error messages
- OrderService.getAllOrders(): delegates to OrderRepository
  .findAllByOrderByCreatedAtDesc() which uses @EntityGraph to eagerly
  fetch the user relationship in a single query
- OrderService.updateOrderStatus(orderId, statusString): looks up order,
  converts status string to OrderStatus enum via case-insensitive
  valueOf(), saves updated entity
- AdminController /api/admin:
  GET  /orders              → list all orders with user email (admin only)
  PATCH /orders/{id}/status → update order status (admin only)
- toAdminResponse() mapper safely handles null user (empty email fallback)
2026-05-15 12:14:53 +02:00
8217b9c038 feat: wire role-based authorities from JWT into Spring Security
- JwtAuthenticationFilter now extracts the "role" claim from the JWT
  token and creates a SimpleGrantedAuthority("ROLE_" + role.toUpperCase())
  on the authentication token. Previously the authorities list was
  always empty (only userDetails.getAuthorities() which returned List.of())
- SecurityConfig adds .requestMatchers("/api/admin/**").hasRole("ADMIN")
  so admin endpoints require the ROLE_ADMIN authority
- All existing endpoints remain authenticated() only — no existing user
  flow is affected
- Public endpoints (auth, webhooks, vehicles) still permitAll()
2026-05-15 12:14:39 +02:00
fefdea089d refactor: add @ManyToOne User relation to Order entity and @EntityGraph query
- Add @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id",
  insertable = false, updatable = false) to Order entity so ORM can
  navigate order.getUser().getEmail() for admin responses
- Keep userId as writable UUID field; the relationship is read-only
  to preserve backward compatibility with existing setUserId() calls
- Add getUser() / setUser() accessors
- Replace handwritten @Query JOIN FETCH with Spring Data derived method
  findAllByOrderByCreatedAtDesc() annotated with @EntityGraph(attributePaths
  = {"user"}) — same eager-load behavior, zero custom JPQL
- No database schema change: user_id FK already exists
2026-05-15 12:14:28 +02:00
6ab5e2f707 refactor: remove template from order flow
Templates serve as a brand shield (showing the platform facilitates all
kinds of messaging), not as a compose-flow form control. Remove them from
the data model and compose page. Templates will live as branding elements
on the landing page in a future commit.

Backend:
- Remove template field from Order entity (getter/setter removed)
- Remove template from CreateOrderRequest DTO
- Remove template from OrderResponse DTO
- Remove template param from OrderService.createOrder()
- Remove template passthrough in OrderController
- Remove /api/templates permitAll from SecurityConfig
- Edit V5 migration: remove template column from orders table
- Edit V6 migration: remove template from seed data
- Update OrderControllerTest (remove template from assertions/requests)
- Update OrderServiceTest (remove template from createOrder calls)

Frontend:
- Remove template from Order interface in api/orders.ts
- Remove template param from createOrder() function
- Remove template display from OrdersPage.vue cards
- Rewrite ComposePage.vue: remove template selector, keep textarea + preview + submit
- Update ComposePage.spec.ts (remove template tests, add preview/GDPR tests)
- Update OrdersPage.spec.ts (remove template from mock data and display test)
- Update compose.spec.ts E2E (remove template selector interactions)
- Update order-history.spec.ts E2E (remove template names test)
- Fix unused import in Router.spec.ts
- Also includes minor Prettier formatting in AppHeader.spec.ts, AdminPage.vue, authStore.ts
2026-05-14 16:55:59 +02:00
55f0fd8771 feat: add POST /api/orders endpoint with validation
- Create CreateOrderRequest DTO with jakarta.validation annotations
- Validate plate format (ABC123 or ABC12A) via @Pattern regex
- Validate letter text: @NotBlank, @Size(min=1, max=1000)
- Validate template name: optional, @Size(max=50)
- Add POST /api/orders endpoint to OrderController (auth required)
- Return 201 Created with OrderResponse on success
- Add 5 controller tests: no auth (403), create success, invalid plate,
  empty text, text over 1000 chars
- All messages in Swedish (Ogiltigt registreringsnummer, Brevtext krävs, etc.)
2026-05-14 15:45:47 +02:00
32b315654e feat: add order history page with API endpoint and seeded test data
- Create OrderController with GET /api/orders endpoint (authenticated)
- Add OrderResponse DTO (id, plate, template, status, trackingId, createdAt)
- Seed 3 test orders for test@bilhalsning.se via V6 migration (sent, pending_payment, delivered)
- Create OrderControllerTest with 4 tests (auth, empty list, full fields, user not found)
- Create frontend api/orders.ts with typed fetchOrders() client
- Build out OrdersPage.vue with card list: plate, template, status badge, tracking link
- Add 12 Vitest tests for OrdersPage (loading, data, badges, links, empty, error)
- Add 5 Playwright E2E tests (auth guard, seeded data, badges, tracking, templates)
2026-05-14 15:30:36 +02:00
a74bb89824 feat: add Order entity, repository, and service with TDD tests
- Create V5__create_orders_table.sql migration with orders table
  - UUID primary key, user_id FK to users, status CHECK constraint
  - Indexes on user_id and status columns
- Add OrderStatus enum (PENDING_PAYMENT, PAID, LOOKUP_STARTED, SENT, DELIVERED, FAILED)
- Add OrderStatusConverter for JPA VARCHAR persistence
- Create Order entity with fields: id, userId, plate, template, letterText, status, amountPaid, trackingId, timestamps
- Create OrderRepository with findByUserIdOrderByCreatedAtDesc and findByStatus queries
- Create OrderService with createOrder (normalizes plate, sets PENDING_PAYMENT), getOrdersByUserId, getOrderById
- Add OrderNotFoundException with 404 handler in GlobalExceptionHandler
- Write OrderServiceTest with 8 unit tests covering status, UUID generation, plate normalization, and error handling
2026-05-14 14:34:14 +02:00
8a95483fb8 feat: add admin role support to backend JWT authentication
Add role-based access control to the backend authentication system. The User
entity now carries a role field (default 'user'), JWT tokens include a 'role'
claim, and the login endpoint populates it from the database.

Changes:
- User entity: add role column (VARCHAR(20), default 'user') with getter/setter
- JwtService: add generateToken(email, role) overload and extractRole(token)
- AuthController: pass user.getRole() on login, 'user' on register
- Flyway V3: ALTER TABLE users ADD COLUMN role
- Flyway V4: seed admin user (admin@bilhalsning.se, role='admin')
- AuthControllerTest: add tests for admin role in token, role from DB on login
- JwtServiceTest: add tests for role extraction and default role
- UserServiceTest: assert role defaults to 'user' on createUser
2026-05-14 12:38:55 +02:00
ca21c5b659 feat: add seed test user migration
Add Flyway migration V2 that inserts a pre-seeded test user for manual
testing. This avoids having to register a new account every time the
environment is reset.

- Email: test@bilhalsning.se
- Password: test1234
- Password hash: bcrypt ($2b$12$)

The migration uses a plain INSERT (no ON CONFLICT) since it runs on
fresh databases only. H2-compatible — no PostgreSQL-specific syntax.
To re-seed after deletion: docker compose down -v && docker compose up -d
2026-05-13 19:18:19 +02:00
3d4a6daee9 feat: add login endpoint with JWT authentication
Add POST /api/auth/login endpoint that authenticates users by email and
password, returning a JWT token on success. Also fixes a critical bug
where expired or malformed JWT tokens in the Authorization header caused
unhandled exceptions, crashing requests to all endpoints including public
ones like registration.

Changes:
- Add AuthController.login() endpoint with LoginRequest DTO
- Add UserService.authenticate() that validates credentials and throws
  InvalidCredentialsException on failure
- Add InvalidCredentialsException and GlobalExceptionHandler handler
  that maps it to 401 with Swedish error message
- Fix JwtAuthenticationFilter to catch JwtException (expired, malformed)
  and pass through without crashing — the filter now acts as a graceful
  enricher rather than a gatekeeper
- Add 5 controller tests for login endpoint (success, 401, validation)
- Add 4 service tests for authenticate() (success, email not found,
  password mismatch, email normalization)
- Add 2 filter tests for expired and malformed token pass-through
2026-05-13 19:16:19 +02:00
8e495672d3 feat: add user registration flow (backend + frontend)
Implement end-to-end registration: POST /api/auth/register creates a
user, returns a JWT, and the frontend RegisterPage stores the token
and redirects to home.

Backend:
- Add AuthController with POST /api/auth/register endpoint
- Add RegisterRequest record (@Email, @NotBlank, @Size(min=8))
- Add AuthResponse and ErrorResponse DTOs
- Add GlobalExceptionHandler (@RestControllerAdvice with logging)
  - EmailAlreadyExistsException -> 409 (Swedish message)
  - MethodArgumentNotValidException -> 400 (field errors)
  - Generic Exception -> 500 (Swedish message + server-side log)

Frontend:
- Add api/client.ts: centralized fetch wrapper with Bearer token
  interceptor, ApiError class, JSON error parsing
- Add api/auth.ts: register() function
- Add stores/authStore.ts: Pinia store with token persistence via
  localStorage, registerUser/logout/isAuthenticated
- Add pages/RegisterPage.vue: email + password + confirm password
  form with client-side validation, submit handler, error display,
  redirect to home on success
- Add route /registrera pointing to RegisterPage
- Add 'Registrera' link to AppHeader navigation

Infrastructure:
- Add __tests__/setup.ts: localStorage polyfill for jsdom 29
  (jsdom 29 lacks standard Storage method implementations)
- Register polyfill via vitest config setupFiles

Tests (17 new, 2 extended):
- AuthControllerTest (@SpringBootTest + @AutoConfigureMockMvc):
  5 backend tests (success 201, duplicate 409, invalid email 400,
  short password 400, missing email 400)
- authStore.spec.ts: 5 tests (unauthenticated start, localStorage
  restore, register success, register failure, logout)
- RegisterPage.spec.ts: 12 tests (render, validation, submit,
  redirect, error display, login link)
- AppHeader.spec.ts: added 'Registrera' link test
- Router.spec.ts: added /registrera route resolution test

Build: 95 tests pass (57 frontend + 38 backend), lint clean.
2026-05-01 19:37:39 +02:00
d70196112d refactor: move Gradle wrapper to repo root, add convenience tasks
Move gradlew, gradle/wrapper, and settings.gradle from backend/ to
the repo root so build commands run from the top-level directory.
This follows the standard multi-project Gradle layout where the build
tool lives alongside docker-compose.yml and all submodules.

- Move gradlew + gradle/wrapper/* from backend/ to repo root
- Move settings.gradle to root with rootProject.name and include 'backend'
- Create root build.gradle with convenience tasks: check, up, down, reset
- check task chains frontend lint → frontend test → backend check
- Update docker-compose.yml backend volume from ./backend:/app to .:/app
- Update backend.Dockerfile entrypoint to ./gradlew :backend:bootRun
- Update AGENTS.md: document ./gradlew check, up, down, reset
- Delete backend/settings.gradle (now at root)
- Add .gradle/ and build/ to .gitignore
- Add !gradle/wrapper/gradle-wrapper.jar exception (blocked by *.jar rule)

All 38 frontend tests and 33 backend tests pass via ./gradlew check.
2026-05-01 18:40:18 +02:00
ce95a451ce feat: implement JWT authentication — service, filter, SecurityFilterChain 2026-05-01 17:38:17 +02:00
0d9baeb6e5 feat: add Subscription enum, converter, entity lifecycle hooks, and ORM-only test rule 2026-05-01 17:38:11 +02:00
c6e2e509eb chore: add JWT secret env config, jjwt deps, and docker-compose prod fixes 2026-05-01 17:38:03 +02:00
c03b5a1401 feat: add User entity, repository, service, and Flyway users table migration
- V1__create_users_table.sql replaces placeholder: creates users table with
  id UUID PK, email UNIQUE NOT NULL, password_hash NOT NULL, subscription
  VARCHAR(20) DEFAULT 'none' with CHECK constraint (none/basic/pro),
  created_at/updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP.
  Compatible with both H2 and PostgreSQL.

- SecurityConfig: minimal @Configuration providing BCryptPasswordEncoder
  bean. Required because Spring Boot 4 no longer auto-configures a
  PasswordEncoder.

- Subscription enum: NONE, BASIC, PRO with string values matching the DB
  CHECK constraint.

- User entity: @PrePersist generates UUID and timestamps in application
  code, @PreUpdate refreshes updated_at. Email setter normalizes to
  lowercase for case-insensitive uniqueness. Explicit getters/setters
  (no Lombok per guidelines).

- UserRepository: Spring Data JPA extending JpaRepository<User, UUID>.
  findByEmail(Optional) and existsByEmail for duplicate checks.

- UserService: @RequiredArgsConstructor with constructor-injected
  UserRepository and PasswordEncoder. createUser normalizes email,
  checks duplicates via existsByEmail, throws EmailAlreadyExistsException,
  hashes password with BCrypt, saves. findByEmail returns Optional<User>.

- EmailAlreadyExistsException: custom RuntimeException for duplicate
  registration attempts. ControllerAdvice handler deferred to auth ticket.

Verification: ./gradlew test passes (Flyway + H2 context loads).
docker compose up -d succeeds, Flyway applies V1 against PostgreSQL 16.
\d users confirms all columns, constraints, defaults, and indexes.
2026-05-01 02:06:24 +02:00
4d449d54d0 feat: add Docker Compose setup with dev and prod configurations
- docker-compose.yml (dev): 3 services — postgres:16, backend (gradle
  bootRun with JDK 21, spring-boot-devtools), frontend (Vite HMR on
  node:24-alpine). Source volume mounts for live editing, Gradle cache
  volume for fast rebuilds, pg_isready healthcheck on postgres.

- docker-compose.prod.yml (prod): same 3 services but with multi-stage
  Dockerfiles. Backend: Gradle bootJar → JRE Alpine, non-root user.
  Frontend: npm ci + vite build → nginx:alpine serving static dist/.
  SSL termination via self-signed cert (auto-generated in entrypoint).
  No source mounts, restart: unless-stopped, separate volumes.

- application-docker.yml: Spring profile overriding H2 with PostgreSQL
  via env vars. Hostname "postgres" resolved by Docker Compose DNS.

- Vite proxy /api → backend:8080 for dev. nginx nginx.conf handles
  /api proxy + SPA fallback + gzip + SSL in prod.

- AGENTS.md, README.md: architecture diagram, dev vs prod comparison
  table, Spring profiles docs, file reference updates.
2026-05-01 01:45:07 +02:00
83b578ca22 feat: scaffold Spring Boot 4 backend with Gradle, Flyway, and H2
- Generate from Spring Initializr with Gradle Groovy DSL, Java 21, Spring Boot 4.0.6
- Dependencies: Web, Security, Data JPA, PostgreSQL Driver, Flyway, Validation, Lombok
- Add H2 runtime dependency for zero-setup local development
- Configure application.yml: H2 in-memory database, port 8080, Flyway with ddl-auto=validate
- Create placeholder Flyway migration V1__init_schema.sql
- Verify ./gradlew test passes and ./gradlew bootRun starts on port 8080
- Update AGENTS.md and README.md: Maven → Gradle commands, Spring Boot 3 → 4
2026-05-01 00:28:10 +02:00