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
This commit is contained in:
Joakim Mörling 2026-05-14 12:38:55 +02:00
parent bb4bb0c6c6
commit 8a95483fb8
8 changed files with 76 additions and 4 deletions

View file

@ -26,14 +26,14 @@ public class AuthController {
@PostMapping("/register")
public ResponseEntity<AuthResponse> 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<AuthResponse> 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));
}
}

View file

@ -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;
}

View file

@ -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()

View file

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'user';

View file

@ -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'
);

View file

@ -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"))

View file

@ -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);
}
}

View file

@ -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));
}