diff --git a/backend/src/main/java/se/bilhalsning/controller/AuthController.java b/backend/src/main/java/se/bilhalsning/controller/AuthController.java index 273ade2..9a6b96e 100644 --- a/backend/src/main/java/se/bilhalsning/controller/AuthController.java +++ b/backend/src/main/java/se/bilhalsning/controller/AuthController.java @@ -26,14 +26,14 @@ public class AuthController { @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { userService.createUser(request.email(), request.password()); - String token = jwtService.generateToken(request.email().toLowerCase().trim()); + String token = jwtService.generateToken(request.email().toLowerCase().trim(), "user"); return ResponseEntity.status(HttpStatus.CREATED).body(new AuthResponse(token)); } @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody LoginRequest request) { User user = userService.authenticate(request.email(), request.password()); - String token = jwtService.generateToken(user.getEmail()); + String token = jwtService.generateToken(user.getEmail(), user.getRole()); return ResponseEntity.ok(new AuthResponse(token)); } } diff --git a/backend/src/main/java/se/bilhalsning/entity/User.java b/backend/src/main/java/se/bilhalsning/entity/User.java index f21a14f..c9da277 100644 --- a/backend/src/main/java/se/bilhalsning/entity/User.java +++ b/backend/src/main/java/se/bilhalsning/entity/User.java @@ -28,6 +28,9 @@ public class User { @Column(name = "subscription", nullable = false, length = 20) private Subscription subscription = Subscription.NONE; + @Column(name = "role", nullable = false, length = 20) + private String role = "user"; + @Column(name = "created_at", nullable = false) private Instant createdAt; @@ -83,6 +86,14 @@ public class User { this.subscription = subscription; } + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + public Instant getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/se/bilhalsning/security/JwtService.java b/backend/src/main/java/se/bilhalsning/security/JwtService.java index c9ee269..4d3a726 100644 --- a/backend/src/main/java/se/bilhalsning/security/JwtService.java +++ b/backend/src/main/java/se/bilhalsning/security/JwtService.java @@ -24,8 +24,13 @@ public class JwtService { } public String generateToken(String email) { + return generateToken(email, "user"); + } + + public String generateToken(String email, String role) { return Jwts.builder() .subject(email) + .claim("role", role) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expirationMs)) .signWith(secretKey) @@ -41,6 +46,15 @@ public class JwtService { .getSubject(); } + public String extractRole(String token) { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload() + .get("role", String.class); + } + public boolean isTokenValid(String token) { try { Jwts.parser() diff --git a/backend/src/main/resources/db/migration/V3__add_role_to_users.sql b/backend/src/main/resources/db/migration/V3__add_role_to_users.sql new file mode 100644 index 0000000..719deb9 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__add_role_to_users.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'user'; diff --git a/backend/src/main/resources/db/migration/V4__seed_admin_user.sql b/backend/src/main/resources/db/migration/V4__seed_admin_user.sql new file mode 100644 index 0000000..0da0443 --- /dev/null +++ b/backend/src/main/resources/db/migration/V4__seed_admin_user.sql @@ -0,0 +1,8 @@ +INSERT INTO users (id, email, password_hash, subscription, role) +VALUES ( + 'b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a12', + 'admin@bilhalsning.se', + '$2b$12$18UFRDPgHWuw5FYeu6X1ReisFjjuxs5XxDafi6.wZbsywoU7vUaLG', + 'none', + 'admin' +); diff --git a/backend/src/test/java/se/bilhalsning/controller/AuthControllerTest.java b/backend/src/test/java/se/bilhalsning/controller/AuthControllerTest.java index 7204f84..b13d70c 100644 --- a/backend/src/test/java/se/bilhalsning/controller/AuthControllerTest.java +++ b/backend/src/test/java/se/bilhalsning/controller/AuthControllerTest.java @@ -39,7 +39,7 @@ class AuthControllerTest { @Test void shouldReturn201AndTokenWhenRegisterSucceeds() throws Exception { when(userService.createUser("new@example.com", "password123")).thenReturn(null); - when(jwtService.generateToken("new@example.com")).thenReturn("test-jwt-token"); + when(jwtService.generateToken("new@example.com", "user")).thenReturn("test-jwt-token"); RegisterRequest request = new RegisterRequest("new@example.com", "password123"); mockMvc.perform(post("/api/auth/register") @@ -93,8 +93,9 @@ class AuthControllerTest { void shouldReturn200AndTokenWhenLoginSucceeds() throws Exception { User user = new User(); user.setEmail("user@example.com"); + user.setRole("user"); when(userService.authenticate("user@example.com", "password123")).thenReturn(user); - when(jwtService.generateToken("user@example.com")).thenReturn("login-jwt-token"); + when(jwtService.generateToken("user@example.com", "user")).thenReturn("login-jwt-token"); LoginRequest request = new LoginRequest("user@example.com", "password123"); mockMvc.perform(post("/api/auth/login") @@ -104,6 +105,22 @@ class AuthControllerTest { .andExpect(jsonPath("$.token").value("login-jwt-token")); } + @Test + void shouldReturnAdminRoleInTokenWhenAdminLogsIn() throws Exception { + User admin = new User(); + admin.setEmail("admin@bilhalsning.se"); + admin.setRole("admin"); + when(userService.authenticate("admin@bilhalsning.se", "admin1234")).thenReturn(admin); + when(jwtService.generateToken("admin@bilhalsning.se", "admin")).thenReturn("admin-jwt-token"); + + LoginRequest request = new LoginRequest("admin@bilhalsning.se", "admin1234"); + mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").value("admin-jwt-token")); + } + @Test void shouldReturn401WhenCredentialsAreInvalid() throws Exception { when(userService.authenticate("user@example.com", "wrongpassword")) diff --git a/backend/src/test/java/se/bilhalsning/security/JwtServiceTest.java b/backend/src/test/java/se/bilhalsning/security/JwtServiceTest.java index 924edc3..12d894b 100644 --- a/backend/src/test/java/se/bilhalsning/security/JwtServiceTest.java +++ b/backend/src/test/java/se/bilhalsning/security/JwtServiceTest.java @@ -71,4 +71,24 @@ class JwtServiceTest { assertFalse(jwtService.isTokenValid(tampered)); } + + @Test + void shouldExtractUserRoleFromToken() { + JwtService jwtService = new JwtService(SECRET); + + String token = jwtService.generateToken(EMAIL, "admin"); + String role = jwtService.extractRole(token); + + assertEquals("admin", role); + } + + @Test + void shouldDefaultToUserRoleWhenNoRoleSpecified() { + JwtService jwtService = new JwtService(SECRET); + + String token = jwtService.generateToken(EMAIL); + String role = jwtService.extractRole(token); + + assertEquals("user", role); + } } diff --git a/backend/src/test/java/se/bilhalsning/service/UserServiceTest.java b/backend/src/test/java/se/bilhalsning/service/UserServiceTest.java index 784de1e..e80c8f2 100644 --- a/backend/src/test/java/se/bilhalsning/service/UserServiceTest.java +++ b/backend/src/test/java/se/bilhalsning/service/UserServiceTest.java @@ -49,6 +49,7 @@ class UserServiceTest { assertEquals("new@example.com", result.getEmail()); assertEquals("hashed", result.getPasswordHash()); assertEquals(Subscription.NONE, result.getSubscription()); + assertEquals("user", result.getRole()); verify(userRepository).save(any(User.class)); }