From 6a75c32da6bb99baeb33c2e7f283931f357379da Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 7 Apr 2026 14:34:30 +0200 Subject: [PATCH 1/5] Adds logic for role mapping. --- .../application/service/CaseNoteMapper.java | 8 ++- .../application/service/CaseService.java | 8 ++- .../application/service/EmployeeMapper.java | 10 ++++ .../application/service/EmployeeService.java | 4 +- .../projektarendehantering/common/Actor.java | 2 +- .../persistence/CaseNoteEntity.java | 4 +- .../persistence/EmployeeEntity.java | 8 ++- .../security/SecurityActorAdapter.java | 29 ++++++++++- .../presentation/dto/CaseNoteDTO.java | 20 +++++-- .../presentation/dto/EmployeeCreateDTO.java | 9 +++- .../presentation/dto/EmployeeDTO.java | 7 ++- .../web/EmployeeUiController.java | 52 +++++++++++++++++++ .../web/GlobalControllerAdvice.java | 25 +++++++++ .../presentation/web/UiController.java | 5 +- .../resources/templates/cases/detail.html | 2 +- .../resources/templates/employees/list.html | 42 +++++++++++++++ .../resources/templates/employees/new.html | 41 +++++++++++++++ .../resources/templates/fragments/header.html | 4 +- 18 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java create mode 100644 src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java create mode 100644 src/main/resources/templates/employees/list.html create mode 100644 src/main/resources/templates/employees/new.html diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java b/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java index a6a8f41..affb3e3 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseNoteMapper.java @@ -12,7 +12,9 @@ public CaseNoteDTO toDTO(CaseNoteEntity entity) { return new CaseNoteDTO( entity.getId(), entity.getContent(), - entity.getAuthor(), + entity.getAuthorDisplayName(), + entity.getAuthorGithubUsername(), + entity.getAuthorRole(), entity.getCreatedAt() ); } @@ -22,7 +24,9 @@ public CaseNoteEntity toEntity(CaseNoteDTO dto) { CaseNoteEntity entity = new CaseNoteEntity(); entity.setId(dto.getId()); entity.setContent(dto.getContent()); - entity.setAuthor(dto.getAuthor()); + entity.setAuthorDisplayName(dto.getAuthorDisplayName()); + entity.setAuthorGithubUsername(dto.getAuthorGithubUsername()); + entity.setAuthorRole(dto.getAuthorRole()); entity.setCreatedAt(dto.getCreatedAt()); return entity; } diff --git a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java index 9482c7c..e5f443c 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/CaseService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/CaseService.java @@ -45,14 +45,18 @@ public CaseService(CaseRepository caseRepository, CaseMapper caseMapper, Patient } @Transactional - public void addNote(UUID caseId, String content, String author) { + public void addNote(UUID caseId, String content, Actor actor) { CaseEntity caseEntity = caseRepository.findById(caseId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Case not found")); CaseNoteEntity note = new CaseNoteEntity(); note.setCaseEntity(caseEntity); note.setContent(content); - note.setAuthor(author); + if (actor != null) { + note.setAuthorDisplayName(actor.displayName()); + note.setAuthorGithubUsername(actor.githubUsername()); + note.setAuthorRole(actor.role() != null ? actor.role().name() : null); + } note.setCreatedAt(Instant.now()); caseNoteRepository.save(note); diff --git a/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java b/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java index 5e43087..2df4c2f 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java +++ b/src/main/java/org/example/projektarendehantering/application/service/EmployeeMapper.java @@ -5,6 +5,9 @@ import org.example.projektarendehantering.presentation.dto.EmployeeDTO; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + @Component public class EmployeeMapper { @@ -13,6 +16,7 @@ public EmployeeDTO toDTO(EmployeeEntity entity) { return new EmployeeDTO( entity.getId(), entity.getDisplayName(), + entity.getGithubUsername(), entity.getRole(), entity.getCreatedAt() ); @@ -22,7 +26,13 @@ public EmployeeEntity toEntity(EmployeeCreateDTO dto) { if (dto == null) return null; EmployeeEntity entity = new EmployeeEntity(); entity.setDisplayName(dto.getDisplayName()); + entity.setGithubUsername(dto.getGithubUsername()); entity.setRole(dto.getRole()); + + if (dto.getGithubUsername() != null && !dto.getGithubUsername().isBlank()) { + UUID id = UUID.nameUUIDFromBytes(dto.getGithubUsername().getBytes(StandardCharsets.UTF_8)); + entity.setId(id); + } return entity; } } diff --git a/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java b/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java index 8304dde..03ba0e6 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java @@ -31,7 +31,9 @@ public EmployeeService(EmployeeRepository employeeRepository, EmployeeMapper emp public EmployeeDTO createEmployee(Actor actor, EmployeeCreateDTO dto) { requireCanManageEmployees(actor); EmployeeEntity entity = employeeMapper.toEntity(dto); - entity.setId(UUID.randomUUID()); + if (entity.getId() == null) { + entity.setId(UUID.randomUUID()); + } entity.setCreatedAt(Instant.now()); return employeeMapper.toDTO(employeeRepository.save(entity)); } diff --git a/src/main/java/org/example/projektarendehantering/common/Actor.java b/src/main/java/org/example/projektarendehantering/common/Actor.java index 1b701e2..a7ea168 100644 --- a/src/main/java/org/example/projektarendehantering/common/Actor.java +++ b/src/main/java/org/example/projektarendehantering/common/Actor.java @@ -2,7 +2,7 @@ import java.util.UUID; -public record Actor(UUID userId, Role role) { +public record Actor(UUID userId, Role role, String displayName, String githubUsername) { } diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java index 1eac7a2..f23fab6 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/CaseNoteEntity.java @@ -27,7 +27,9 @@ public class CaseNoteEntity { private String content; - private String author; + private String authorDisplayName; + private String authorGithubUsername; + private String authorRole; private Instant createdAt; diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java index 2c87e48..cb36aa7 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java @@ -19,6 +19,8 @@ public class EmployeeEntity { private String displayName; + private String githubUsername; + @Enumerated(EnumType.STRING) private Role role; @@ -26,9 +28,10 @@ public class EmployeeEntity { public EmployeeEntity() {} - public EmployeeEntity(UUID id, String displayName, Role role, Instant createdAt) { + public EmployeeEntity(UUID id, String displayName, String githubUsername, Role role, Instant createdAt) { this.id = id; this.displayName = displayName; + this.githubUsername = githubUsername; this.role = role; this.createdAt = createdAt; } @@ -39,6 +42,9 @@ public EmployeeEntity(UUID id, String displayName, Role role, Instant createdAt) public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getGithubUsername() { return githubUsername; } + public void setGithubUsername(String githubUsername) { this.githubUsername = githubUsername; } + public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java b/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java index fb8d7da..eda381a 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapter.java @@ -3,8 +3,10 @@ import org.example.projektarendehantering.common.Actor; import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; @@ -16,6 +18,12 @@ @Component public class SecurityActorAdapter { + private final EmployeeRepository employeeRepository; + + public SecurityActorAdapter(EmployeeRepository employeeRepository) { + this.employeeRepository = employeeRepository; + } + public Actor currentUser() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -23,9 +31,26 @@ public Actor currentUser() { throw new NotAuthorizedException("User not authenticated"); } + // Try to find the username (GitHub 'login' attribute) or fallback to name + String name = authentication.getName(); + if (authentication instanceof OAuth2AuthenticationToken oauth2Token) { + String login = oauth2Token.getPrincipal().getAttribute("login"); + if (login != null) { + name = login; + } + } + // Create a deterministic UUID based on the username/name - UUID userId = UUID.nameUUIDFromBytes(authentication.getName().getBytes(StandardCharsets.UTF_8)); + UUID userId = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); + + // 1. Try finding an employee with this UUID + var employee = employeeRepository.findById(userId); + if (employee.isPresent()) { + var e = employee.get(); + return new Actor(userId, e.getRole(), e.getDisplayName(), e.getGithubUsername()); + } + // 2. Fallback to existing logic (checking Spring authorities) Role role = Role.PATIENT; if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_MANAGER"))) { role = Role.MANAGER; @@ -37,6 +62,6 @@ public Actor currentUser() { role = Role.PATIENT; } - return new Actor(userId, role); + return new Actor(userId, role, null, name); } } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java index 2f5660c..7619de3 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/CaseNoteDTO.java @@ -7,15 +7,19 @@ public class CaseNoteDTO { private UUID id; private String content; - private String author; + private String authorDisplayName; + private String authorGithubUsername; + private String authorRole; private Instant createdAt; public CaseNoteDTO() {} - public CaseNoteDTO(UUID id, String content, String author, Instant createdAt) { + public CaseNoteDTO(UUID id, String content, String authorDisplayName, String authorGithubUsername, String authorRole, Instant createdAt) { this.id = id; this.content = content; - this.author = author; + this.authorDisplayName = authorDisplayName; + this.authorGithubUsername = authorGithubUsername; + this.authorRole = authorRole; this.createdAt = createdAt; } @@ -25,8 +29,14 @@ public CaseNoteDTO(UUID id, String content, String author, Instant createdAt) { public String getContent() { return content; } public void setContent(String content) { this.content = content; } - public String getAuthor() { return author; } - public void setAuthor(String author) { this.author = author; } + public String getAuthorDisplayName() { return authorDisplayName; } + public void setAuthorDisplayName(String authorDisplayName) { this.authorDisplayName = authorDisplayName; } + + public String getAuthorGithubUsername() { return authorGithubUsername; } + public void setAuthorGithubUsername(String authorGithubUsername) { this.authorGithubUsername = authorGithubUsername; } + + public String getAuthorRole() { return authorRole; } + public void setAuthorRole(String authorRole) { this.authorRole = authorRole; } public Instant getCreatedAt() { return createdAt; } public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java index 1b542f4..94fdecc 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeCreateDTO.java @@ -9,19 +9,26 @@ public class EmployeeCreateDTO { @NotBlank private String displayName; + @NotBlank + private String githubUsername; + @NotNull private Role role; public EmployeeCreateDTO() {} - public EmployeeCreateDTO(String displayName, Role role) { + public EmployeeCreateDTO(String displayName, String githubUsername, Role role) { this.displayName = displayName; + this.githubUsername = githubUsername; this.role = role; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getGithubUsername() { return githubUsername; } + public void setGithubUsername(String githubUsername) { this.githubUsername = githubUsername; } + public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } } diff --git a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java index fcecf7f..b502395 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java +++ b/src/main/java/org/example/projektarendehantering/presentation/dto/EmployeeDTO.java @@ -9,14 +9,16 @@ public class EmployeeDTO { private UUID id; private String displayName; + private String githubUsername; private Role role; private Instant createdAt; public EmployeeDTO() {} - public EmployeeDTO(UUID id, String displayName, Role role, Instant createdAt) { + public EmployeeDTO(UUID id, String displayName, String githubUsername, Role role, Instant createdAt) { this.id = id; this.displayName = displayName; + this.githubUsername = githubUsername; this.role = role; this.createdAt = createdAt; } @@ -27,6 +29,9 @@ public EmployeeDTO(UUID id, String displayName, Role role, Instant createdAt) { public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } + public String getGithubUsername() { return githubUsername; } + public void setGithubUsername(String githubUsername) { this.githubUsername = githubUsername; } + public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java new file mode 100644 index 0000000..2b7a88e --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java @@ -0,0 +1,52 @@ +package org.example.projektarendehantering.presentation.web; + +import jakarta.validation.Valid; +import org.example.projektarendehantering.application.service.EmployeeService; +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.example.projektarendehantering.presentation.dto.EmployeeCreateDTO; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class EmployeeUiController { + + private final EmployeeService employeeService; + private final SecurityActorAdapter securityActorAdapter; + + public EmployeeUiController(EmployeeService employeeService, SecurityActorAdapter securityActorAdapter) { + this.employeeService = employeeService; + this.securityActorAdapter = securityActorAdapter; + } + + @GetMapping("/ui/employees") + public String listEmployees(Model model) { + Actor actor = securityActorAdapter.currentUser(); + model.addAttribute("employees", employeeService.getAllEmployees(actor)); + return "employees/list"; + } + + @GetMapping("/ui/employees/new") + public String newEmployee(Model model) { + model.addAttribute("employeeCreateDTO", new EmployeeCreateDTO()); + model.addAttribute("roles", Role.values()); + return "employees/new"; + } + + @PostMapping("/ui/employees/new") + public String createEmployee(@Valid @ModelAttribute("employeeCreateDTO") EmployeeCreateDTO dto, BindingResult result, Model model) { + if (result.hasErrors()) { + model.addAttribute("roles", Role.values()); + return "employees/new"; + } + + Actor actor = securityActorAdapter.currentUser(); + employeeService.createEmployee(actor, dto); + return "redirect:/ui/employees"; + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java b/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java new file mode 100644 index 0000000..eaa8372 --- /dev/null +++ b/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java @@ -0,0 +1,25 @@ +package org.example.projektarendehantering.presentation.web; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +@ControllerAdvice +public class GlobalControllerAdvice { + + private final SecurityActorAdapter securityActorAdapter; + + public GlobalControllerAdvice(SecurityActorAdapter securityActorAdapter) { + this.securityActorAdapter = securityActorAdapter; + } + + @ModelAttribute("currentActor") + public Actor currentActor() { + try { + return securityActorAdapter.currentUser(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java index 6e91bed..3d49529 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/UiController.java @@ -32,9 +32,8 @@ public UiController(CaseService caseService, PatientService patientService, Secu } @PostMapping("/ui/cases/{caseId}/notes") - public String addNote(@PathVariable UUID caseId, @RequestParam("content") String content, Principal principal) { - String author = principal != null ? principal.getName() : "Anonymous"; - caseService.addNote(caseId, content, author); + public String addNote(@PathVariable UUID caseId, @RequestParam("content") String content) { + caseService.addNote(caseId, content, securityActorAdapter.currentUser()); return "redirect:/ui/cases/" + caseId; } diff --git a/src/main/resources/templates/cases/detail.html b/src/main/resources/templates/cases/detail.html index dd18676..880a592 100644 --- a/src/main/resources/templates/cases/detail.html +++ b/src/main/resources/templates/cases/detail.html @@ -31,7 +31,7 @@

Notes

Note content

- Author - + Author - Date
diff --git a/src/main/resources/templates/employees/list.html b/src/main/resources/templates/employees/list.html new file mode 100644 index 0000000..d9f35a8 --- /dev/null +++ b/src/main/resources/templates/employees/list.html @@ -0,0 +1,42 @@ + + + + + +
+ +
+
+

User Mappings (Employees)

+ New Mapping +
+ +
+

List of GitHub users mapped to application roles.

+ + + + + + + + + + + + + + + + + + + +
Display NameGitHub UsernameRoleID (UUID)Created At
NameUsernameRoleUUIDDate
+

No user mappings found.

+
+
+ +
+ + diff --git a/src/main/resources/templates/employees/new.html b/src/main/resources/templates/employees/new.html new file mode 100644 index 0000000..b199545 --- /dev/null +++ b/src/main/resources/templates/employees/new.html @@ -0,0 +1,41 @@ + + + + + +
+ +
+
+

New User Mapping

+ Back to list +
+ +
+ + + +
+ +
+
+
+ +
+ + diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html index 4a4756b..a433a1e 100644 --- a/src/main/resources/templates/fragments/header.html +++ b/src/main/resources/templates/fragments/header.html @@ -11,11 +11,13 @@ Cases Create Audit + Manage Users
- User + User
From c2d95a123a7a125433893c36f7f3e63d91de5225 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 7 Apr 2026 14:41:00 +0200 Subject: [PATCH 2/5] Add unit tests for `SecurityActorAdapter` --- .../security/SecurityActorAdapterTest.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java diff --git a/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java new file mode 100644 index 0000000..dc6146d --- /dev/null +++ b/src/test/java/org/example/projektarendehantering/infrastructure/security/SecurityActorAdapterTest.java @@ -0,0 +1,161 @@ +package org.example.projektarendehantering.infrastructure.security; + +import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; +import org.example.projektarendehantering.common.Role; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; +import org.example.projektarendehantering.infrastructure.persistence.EmployeeRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SecurityActorAdapterTest { + + @Mock + private EmployeeRepository employeeRepository; + + @Mock + private Authentication authentication; + + @Mock + private SecurityContext securityContext; + + @InjectMocks + private SecurityActorAdapter securityActorAdapter; + + @BeforeEach + void setUp() { + SecurityContextHolder.setContext(securityContext); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void currentUser_whenNotAuthenticated_shouldThrowException() { + when(securityContext.getAuthentication()).thenReturn(null); + + assertThrows(NotAuthorizedException.class, () -> securityActorAdapter.currentUser()); + } + + @Test + void currentUser_whenAnonymousUser_shouldThrowException() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("anonymousUser"); + + assertThrows(NotAuthorizedException.class, () -> securityActorAdapter.currentUser()); + } + + @Test + void currentUser_whenEmployeeFoundInRepository_shouldReturnActorFromEmployee() { + String username = "testuser"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes()); + EmployeeEntity employee = new EmployeeEntity(userId, "Test User", username, Role.DOCTOR, Instant.now()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.of(employee)); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(userId, actor.userId()); + assertEquals(Role.DOCTOR, actor.role()); + assertEquals("Test User", actor.displayName()); + assertEquals(username, actor.githubUsername()); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Manager() { + String username = "manager-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_MANAGER"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.MANAGER, actor.role()); + assertEquals(username, actor.githubUsername()); + assertNull(actor.displayName()); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Doctor() { + String username = "doctor-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_DOCTOR"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.DOCTOR, actor.role()); + } + + @Test + void currentUser_whenEmployeeNotFound_shouldFallbackToAuthorities_Nurse() { + String username = "nurse-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(List.of(new SimpleGrantedAuthority("ROLE_NURSE"))) + .when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.NURSE, actor.role()); + } + + @Test + void currentUser_whenEmployeeNotFoundAndNoRoles_shouldDefaultToPatient() { + String username = "patient-user"; + UUID userId = UUID.nameUUIDFromBytes(username.getBytes()); + + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn(username); + when(employeeRepository.findById(userId)).thenReturn(Optional.empty()); + + doReturn(Collections.emptyList()).when(authentication).getAuthorities(); + + Actor actor = securityActorAdapter.currentUser(); + + assertEquals(Role.PATIENT, actor.role()); + } +} From 9edf28af55506045398b801b450e1d472323b5e4 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 7 Apr 2026 15:10:25 +0200 Subject: [PATCH 3/5] Enforce unique GitHub username for employees and add validation in `createEmployee` service --- .../application/service/EmployeeService.java | 6 ++++++ .../infrastructure/persistence/EmployeeEntity.java | 2 ++ .../infrastructure/persistence/EmployeeRepository.java | 2 ++ 3 files changed, 10 insertions(+) diff --git a/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java b/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java index 03ba0e6..6d4b4c6 100644 --- a/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java +++ b/src/main/java/org/example/projektarendehantering/application/service/EmployeeService.java @@ -1,6 +1,7 @@ package org.example.projektarendehantering.application.service; import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.BadRequestException; import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.common.Role; import org.example.projektarendehantering.infrastructure.persistence.EmployeeEntity; @@ -30,6 +31,11 @@ public EmployeeService(EmployeeRepository employeeRepository, EmployeeMapper emp @Transactional public EmployeeDTO createEmployee(Actor actor, EmployeeCreateDTO dto) { requireCanManageEmployees(actor); + + if (employeeRepository.findByGithubUsername(dto.getGithubUsername()).isPresent()) { + throw new BadRequestException("EMPLOYEE_EXISTS", "Employee with username " + dto.getGithubUsername() + " already exists"); + } + EmployeeEntity entity = employeeMapper.toEntity(dto); if (entity.getId() == null) { entity.setId(UUID.randomUUID()); diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java index cb36aa7..1e46f99 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeEntity.java @@ -1,5 +1,6 @@ package org.example.projektarendehantering.infrastructure.persistence; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -19,6 +20,7 @@ public class EmployeeEntity { private String displayName; + @Column(unique = true) private String githubUsername; @Enumerated(EnumType.STRING) diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java index 3e23e9d..081233b 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/persistence/EmployeeRepository.java @@ -2,8 +2,10 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; public interface EmployeeRepository extends JpaRepository { + Optional findByGithubUsername(String githubUsername); } From d432fca3c0e1a53e29a3295dbf76baf4ef141db6 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 7 Apr 2026 15:16:37 +0200 Subject: [PATCH 4/5] Enable method-level security and restrict new employee creation to managers. --- .../infrastructure/config/SecurityConfig.java | 2 ++ .../presentation/web/EmployeeUiController.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java b/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java index ca77fca..9b227de 100644 --- a/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java +++ b/src/main/java/org/example/projektarendehantering/infrastructure/config/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; @@ -12,6 +13,7 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity public class SecurityConfig { @Bean diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java b/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java index 2b7a88e..9b3c467 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/EmployeeUiController.java @@ -6,6 +6,7 @@ import org.example.projektarendehantering.common.Role; import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; import org.example.projektarendehantering.presentation.dto.EmployeeCreateDTO; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -32,6 +33,7 @@ public String listEmployees(Model model) { } @GetMapping("/ui/employees/new") + @PreAuthorize("hasRole('MANAGER')") public String newEmployee(Model model) { model.addAttribute("employeeCreateDTO", new EmployeeCreateDTO()); model.addAttribute("roles", Role.values()); From 85dd8f012789cc2662321913a6732404821d1b46 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 7 Apr 2026 15:27:43 +0200 Subject: [PATCH 5/5] Rabbit feedback --- .../web/GlobalControllerAdvice.java | 3 +- .../resources/templates/employees/new.html | 8 ++-- .../resources/templates/fragments/header.html | 11 ++++-- .../security/SecurityActorAdapterTest.java | 37 ++++++++++++++++--- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java b/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java index eaa8372..d209135 100644 --- a/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java +++ b/src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java @@ -1,6 +1,7 @@ package org.example.projektarendehantering.presentation.web; import org.example.projektarendehantering.common.Actor; +import org.example.projektarendehantering.common.NotAuthorizedException; import org.example.projektarendehantering.infrastructure.security.SecurityActorAdapter; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; @@ -18,7 +19,7 @@ public GlobalControllerAdvice(SecurityActorAdapter securityActorAdapter) { public Actor currentActor() { try { return securityActorAdapter.currentUser(); - } catch (Exception e) { + } catch (NotAuthorizedException e) { return null; } } diff --git a/src/main/resources/templates/employees/new.html b/src/main/resources/templates/employees/new.html index b199545..fa717c4 100644 --- a/src/main/resources/templates/employees/new.html +++ b/src/main/resources/templates/employees/new.html @@ -8,19 +8,21 @@

New User Mapping

- Back to list + Back to list