feat(audit): emit USER_DELETED when admin removes a user

Adds actorId param to deleteUser(), captures email before deletion,
emits logAfterCommit(USER_DELETED) with userId+email in payload.
Updates UserController to resolve and pass actorId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-26 14:51:15 +02:00
parent 9cd48007a0
commit 81387b2f1b
3 changed files with 32 additions and 5 deletions

View File

@@ -95,8 +95,10 @@ public class UserController {
@DeleteMapping("/users/{id}")
@RequirePermission(Permission.ADMIN_USER)
public ResponseEntity<Void> deleteUser(@PathVariable UUID id) {
userService.deleteUser(id);
public ResponseEntity<Void> deleteUser(Authentication authentication,
@PathVariable UUID id) {
AppUser actor = userService.findByEmail(authentication.getName());
userService.deleteUser(actor.getId(), id);
return ResponseEntity.ok().build();
}

View File

@@ -106,10 +106,13 @@ public class UserService {
}
@Transactional
public void deleteUser(UUID userId) {
public void deleteUser(UUID actorId, UUID userId) {
AppUser user = userRepository.findById(userId)
.orElseThrow(() -> DomainException.notFound(ErrorCode.USER_NOT_FOUND, "No user found for id: " + userId));
String email = user.getEmail();
userRepository.delete(user);
auditService.logAfterCommit(AuditKind.USER_DELETED, actorId, null,
Map.of("userId", userId.toString(), "email", email));
}
public AppUser getById(UUID id) {

View File

@@ -65,7 +65,7 @@ class UserServiceTest {
UUID id = UUID.randomUUID();
when(userRepository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> userService.deleteUser(id))
assertThatThrownBy(() -> userService.deleteUser(UUID.randomUUID(), id))
.isInstanceOf(DomainException.class);
}
@@ -75,7 +75,7 @@ class UserServiceTest {
AppUser user = AppUser.builder().id(id).email("gast@example.com").build();
when(userRepository.findById(id)).thenReturn(Optional.of(user));
userService.deleteUser(id);
userService.deleteUser(UUID.randomUUID(), id);
verify(userRepository).delete(user);
}
@@ -703,6 +703,28 @@ class UserServiceTest {
assertThat(result).containsExactly(g);
}
// ─── audit: USER_DELETED ──────────────────────────────────────────────────
@Test
void deleteUser_logsUserDeleted_withEmailInPayload() {
UUID actorId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
AppUser user = AppUser.builder().id(userId).email("gone@example.com").build();
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
userService.deleteUser(actorId, userId);
@SuppressWarnings("unchecked")
ArgumentCaptor<java.util.Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(java.util.Map.class);
verify(auditService).logAfterCommit(
org.mockito.ArgumentMatchers.eq(AuditKind.USER_DELETED),
org.mockito.ArgumentMatchers.eq(actorId),
org.mockito.ArgumentMatchers.isNull(),
payloadCaptor.capture());
assertThat(payloadCaptor.getValue()).containsEntry("email", "gone@example.com");
assertThat(payloadCaptor.getValue()).containsKey("userId");
}
// ─── audit: USER_CREATED ──────────────────────────────────────────────────
@Test