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
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
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
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.
- 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.
- 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