Commit graph

16 commits

Author SHA1 Message Date
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