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:
parent
bb4bb0c6c6
commit
8a95483fb8
8 changed files with 76 additions and 4 deletions
|
|
@ -26,14 +26,14 @@ public class AuthController {
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||||
userService.createUser(request.email(), request.password());
|
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));
|
return ResponseEntity.status(HttpStatus.CREATED).body(new AuthResponse(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||||
User user = userService.authenticate(request.email(), request.password());
|
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));
|
return ResponseEntity.ok(new AuthResponse(token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ public class User {
|
||||||
@Column(name = "subscription", nullable = false, length = 20)
|
@Column(name = "subscription", nullable = false, length = 20)
|
||||||
private Subscription subscription = Subscription.NONE;
|
private Subscription subscription = Subscription.NONE;
|
||||||
|
|
||||||
|
@Column(name = "role", nullable = false, length = 20)
|
||||||
|
private String role = "user";
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
|
|
||||||
|
|
@ -83,6 +86,14 @@ public class User {
|
||||||
this.subscription = subscription;
|
this.subscription = subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(String role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
public Instant getCreatedAt() {
|
public Instant getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,13 @@ public class JwtService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateToken(String email) {
|
public String generateToken(String email) {
|
||||||
|
return generateToken(email, "user");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(String email, String role) {
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.subject(email)
|
.subject(email)
|
||||||
|
.claim("role", role)
|
||||||
.issuedAt(new Date())
|
.issuedAt(new Date())
|
||||||
.expiration(new Date(System.currentTimeMillis() + expirationMs))
|
.expiration(new Date(System.currentTimeMillis() + expirationMs))
|
||||||
.signWith(secretKey)
|
.signWith(secretKey)
|
||||||
|
|
@ -41,6 +46,15 @@ public class JwtService {
|
||||||
.getSubject();
|
.getSubject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String extractRole(String token) {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload()
|
||||||
|
.get("role", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTokenValid(String token) {
|
public boolean isTokenValid(String token) {
|
||||||
try {
|
try {
|
||||||
Jwts.parser()
|
Jwts.parser()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'user';
|
||||||
|
|
@ -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'
|
||||||
|
);
|
||||||
|
|
@ -39,7 +39,7 @@ class AuthControllerTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldReturn201AndTokenWhenRegisterSucceeds() throws Exception {
|
void shouldReturn201AndTokenWhenRegisterSucceeds() throws Exception {
|
||||||
when(userService.createUser("new@example.com", "password123")).thenReturn(null);
|
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");
|
RegisterRequest request = new RegisterRequest("new@example.com", "password123");
|
||||||
mockMvc.perform(post("/api/auth/register")
|
mockMvc.perform(post("/api/auth/register")
|
||||||
|
|
@ -93,8 +93,9 @@ class AuthControllerTest {
|
||||||
void shouldReturn200AndTokenWhenLoginSucceeds() throws Exception {
|
void shouldReturn200AndTokenWhenLoginSucceeds() throws Exception {
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setEmail("user@example.com");
|
user.setEmail("user@example.com");
|
||||||
|
user.setRole("user");
|
||||||
when(userService.authenticate("user@example.com", "password123")).thenReturn(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");
|
LoginRequest request = new LoginRequest("user@example.com", "password123");
|
||||||
mockMvc.perform(post("/api/auth/login")
|
mockMvc.perform(post("/api/auth/login")
|
||||||
|
|
@ -104,6 +105,22 @@ class AuthControllerTest {
|
||||||
.andExpect(jsonPath("$.token").value("login-jwt-token"));
|
.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
|
@Test
|
||||||
void shouldReturn401WhenCredentialsAreInvalid() throws Exception {
|
void shouldReturn401WhenCredentialsAreInvalid() throws Exception {
|
||||||
when(userService.authenticate("user@example.com", "wrongpassword"))
|
when(userService.authenticate("user@example.com", "wrongpassword"))
|
||||||
|
|
|
||||||
|
|
@ -71,4 +71,24 @@ class JwtServiceTest {
|
||||||
|
|
||||||
assertFalse(jwtService.isTokenValid(tampered));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ class UserServiceTest {
|
||||||
assertEquals("new@example.com", result.getEmail());
|
assertEquals("new@example.com", result.getEmail());
|
||||||
assertEquals("hashed", result.getPasswordHash());
|
assertEquals("hashed", result.getPasswordHash());
|
||||||
assertEquals(Subscription.NONE, result.getSubscription());
|
assertEquals(Subscription.NONE, result.getSubscription());
|
||||||
|
assertEquals("user", result.getRole());
|
||||||
verify(userRepository).save(any(User.class));
|
verify(userRepository).save(any(User.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue