From 19832dc1e0e05d9ed58cc14949ac74dcd55ce638 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 19 Apr 2026 16:39:41 +0200 Subject: [PATCH] refactor(security): extract requireUserId to SecurityUtils Both DocumentController and TranscriptionBlockController contained identical private requireUserId helpers. Extracted to a shared static utility in the security package ahead of DashboardController which also needs actor resolution. Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 10 +--- .../TranscriptionBlockController.java | 12 +--- .../security/SecurityUtils.java | 24 ++++++++ .../security/SecurityUtilsTest.java | 58 +++++++++++++++++++ 4 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/security/SecurityUtils.java create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/security/SecurityUtilsTest.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index e29e164f..493fa046 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -28,6 +28,7 @@ import org.raddatz.familienarchiv.model.DocumentVersion; import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.RequirePermission; +import org.raddatz.familienarchiv.security.SecurityUtils; import org.raddatz.familienarchiv.service.DocumentService; import org.raddatz.familienarchiv.service.DocumentVersionService; import org.raddatz.familienarchiv.service.FileService; @@ -286,13 +287,6 @@ public class DocumentController { } private UUID requireUserId(Authentication authentication) { - if (authentication == null || !authentication.isAuthenticated()) { - throw DomainException.unauthorized("Authentication required"); - } - AppUser user = userService.findByEmail(authentication.getName()); - if (user == null) { - throw DomainException.unauthorized("User not found"); - } - return user.getId(); + return SecurityUtils.requireUserId(authentication, userService); } } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionBlockController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionBlockController.java index 7b36cd26..82338bc2 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionBlockController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/TranscriptionBlockController.java @@ -5,12 +5,11 @@ import lombok.extern.slf4j.Slf4j; import org.raddatz.familienarchiv.dto.CreateTranscriptionBlockDTO; import org.raddatz.familienarchiv.dto.ReorderTranscriptionBlocksDTO; import org.raddatz.familienarchiv.dto.UpdateTranscriptionBlockDTO; -import org.raddatz.familienarchiv.exception.DomainException; -import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.TranscriptionBlock; import org.raddatz.familienarchiv.model.TranscriptionBlockVersion; import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.RequirePermission; +import org.raddatz.familienarchiv.security.SecurityUtils; import org.raddatz.familienarchiv.service.TranscriptionService; import org.raddatz.familienarchiv.service.UserService; import org.springframework.http.HttpStatus; @@ -100,13 +99,6 @@ public class TranscriptionBlockController { } private UUID requireUserId(Authentication authentication) { - if (authentication == null || !authentication.isAuthenticated()) { - throw DomainException.unauthorized("Authentication required"); - } - AppUser user = userService.findByEmail(authentication.getName()); - if (user == null) { - throw DomainException.unauthorized("User not found"); - } - return user.getId(); + return SecurityUtils.requireUserId(authentication, userService); } } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityUtils.java b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityUtils.java new file mode 100644 index 00000000..dd5dceb7 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityUtils.java @@ -0,0 +1,24 @@ +package org.raddatz.familienarchiv.security; + +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.model.AppUser; +import org.raddatz.familienarchiv.service.UserService; +import org.springframework.security.core.Authentication; + +import java.util.UUID; + +public final class SecurityUtils { + + private SecurityUtils() {} + + public static UUID requireUserId(Authentication authentication, UserService userService) { + if (authentication == null || !authentication.isAuthenticated()) { + throw DomainException.unauthorized("Authentication required"); + } + AppUser user = userService.findByEmail(authentication.getName()); + if (user == null) { + throw DomainException.unauthorized("User not found"); + } + return user.getId(); + } +} diff --git a/backend/src/test/java/org/raddatz/familienarchiv/security/SecurityUtilsTest.java b/backend/src/test/java/org/raddatz/familienarchiv/security/SecurityUtilsTest.java new file mode 100644 index 00000000..78f0dc24 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/security/SecurityUtilsTest.java @@ -0,0 +1,58 @@ +package org.raddatz.familienarchiv.security; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.model.AppUser; +import org.raddatz.familienarchiv.service.UserService; +import org.springframework.security.core.Authentication; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SecurityUtilsTest { + + @Mock Authentication authentication; + @Mock UserService userService; + + @Test + void requireUserId_throwsUnauthorized_whenAuthenticationIsNull() { + assertThatThrownBy(() -> SecurityUtils.requireUserId(null, userService)) + .isInstanceOf(DomainException.class); + } + + @Test + void requireUserId_throwsUnauthorized_whenNotAuthenticated() { + when(authentication.isAuthenticated()).thenReturn(false); + assertThatThrownBy(() -> SecurityUtils.requireUserId(authentication, userService)) + .isInstanceOf(DomainException.class); + } + + @Test + void requireUserId_throwsUnauthorized_whenUserNotFound() { + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("ghost@example.com"); + when(userService.findByEmail("ghost@example.com")).thenReturn(null); + assertThatThrownBy(() -> SecurityUtils.requireUserId(authentication, userService)) + .isInstanceOf(DomainException.class); + } + + @Test + void requireUserId_returnsUserId_whenAuthenticated() { + UUID userId = UUID.randomUUID(); + AppUser user = AppUser.builder().id(userId).email("user@example.com").password("pw").build(); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getName()).thenReturn("user@example.com"); + when(userService.findByEmail("user@example.com")).thenReturn(user); + + UUID result = SecurityUtils.requireUserId(authentication, userService); + + assertThat(result).isEqualTo(userId); + } +}