From cf9a8ea39343236adc3746e6cad97f404e4199ab Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 18 Apr 2026 20:31:32 +0200 Subject: [PATCH] feat(auth): switch CustomUserDetailsService to email-based lookup loadUserByUsername now calls findByEmail and returns email as the Spring Security principal name. Tests updated to assert email identity. Co-Authored-By: Claude Sonnet 4.6 --- .../service/CustomUserDetailsService.java | 12 ++--- .../service/CustomUserDetailsServiceTest.java | 52 +++++++++---------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/CustomUserDetailsService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/CustomUserDetailsService.java index e9dd489f..28377e70 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/CustomUserDetailsService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/CustomUserDetailsService.java @@ -29,24 +29,22 @@ public class CustomUserDetailsService implements UserDetailsService { private final AppUserRepository userRepository; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - AppUser appUser = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + username)); + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + AppUser appUser = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User nicht gefunden: " + email)); - // Collect all permissions from all groups; warn about any that don't match a known Permission enum value var authorities = appUser.getGroups().stream() .flatMap(group -> group.getPermissions().stream()) .peek(p -> { if (!KNOWN_PERMISSIONS.contains(p)) { - log.warn("Unknown permission '{}' found in database for user '{}' — it will be granted but never matched by @RequirePermission", p, appUser.getUsername()); + log.warn("Unknown permission '{}' found in database for user '{}' — it will be granted but never matched by @RequirePermission", p, appUser.getEmail()); } }) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); - // Rückgabe des Standard Spring Security User Objekts return new User( - appUser.getUsername(), + appUser.getEmail(), appUser.getPassword(), appUser.isEnabled(), true, true, true, diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/CustomUserDetailsServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/CustomUserDetailsServiceTest.java index 8033faf2..a2cb51b1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/CustomUserDetailsServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/CustomUserDetailsServiceTest.java @@ -8,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.raddatz.familienarchiv.model.AppUser; import org.raddatz.familienarchiv.model.UserGroup; import org.raddatz.familienarchiv.repository.AppUserRepository; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -29,40 +28,40 @@ class CustomUserDetailsServiceTest { // ─── loadUserByUsername — not found ────────────────────────────────────── @Test - void loadUserByUsername_throwsUsernameNotFoundException_whenUserNotFound() { - when(userRepository.findByUsername("ghost")).thenReturn(Optional.empty()); + void loadUserByEmail_throwsUsernameNotFoundException_whenUserNotFound() { + when(userRepository.findByEmail("ghost@example.com")).thenReturn(Optional.empty()); - assertThatThrownBy(() -> service.loadUserByUsername("ghost")) + assertThatThrownBy(() -> service.loadUserByUsername("ghost@example.com")) .isInstanceOf(UsernameNotFoundException.class) - .hasMessageContaining("ghost"); + .hasMessageContaining("ghost@example.com"); } // ─── loadUserByUsername — happy path ───────────────────────────────────── @Test - void loadUserByUsername_returnsUserDetails_withMappedAuthorities() { + void loadUserByEmail_returnsUserDetails_withEmailAsPrincipal() { UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("Admins") .permissions(Set.of("READ_ALL", "WRITE_ALL")).build(); AppUser user = AppUser.builder().id(UUID.randomUUID()) - .username("admin").password("hashed").enabled(true) + .email("admin@example.com").password("hashed").enabled(true) .groups(Set.of(group)).build(); - when(userRepository.findByUsername("admin")).thenReturn(Optional.of(user)); + when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.of(user)); - UserDetails details = service.loadUserByUsername("admin"); + UserDetails details = service.loadUserByUsername("admin@example.com"); - assertThat(details.getUsername()).isEqualTo("admin"); + assertThat(details.getUsername()).isEqualTo("admin@example.com"); assertThat(details.getAuthorities()).extracting("authority") .contains("READ_ALL", "WRITE_ALL"); } @Test - void loadUserByUsername_returnsEmptyAuthorities_whenUserHasNoGroups() { + void loadUserByEmail_returnsEmptyAuthorities_whenUserHasNoGroups() { AppUser user = AppUser.builder().id(UUID.randomUUID()) - .username("viewer").password("hashed").enabled(true) + .email("viewer@example.com").password("hashed").enabled(true) .groups(Set.of()).build(); - when(userRepository.findByUsername("viewer")).thenReturn(Optional.of(user)); + when(userRepository.findByEmail("viewer@example.com")).thenReturn(Optional.of(user)); - UserDetails details = service.loadUserByUsername("viewer"); + UserDetails details = service.loadUserByUsername("viewer@example.com"); assertThat(details.getAuthorities()).isEmpty(); } @@ -70,16 +69,15 @@ class CustomUserDetailsServiceTest { // ─── loadUserByUsername — unknown permission ────────────────────────────── @Test - void loadUserByUsername_grantsUnknownPermission_butLogsWarning() { - // Unknown permissions should still be granted (logged as warning, not silently dropped) + void loadUserByEmail_grantsUnknownPermission_butLogsWarning() { UserGroup group = UserGroup.builder().id(UUID.randomUUID()).name("CustomGroup") .permissions(Set.of("UNKNOWN_CUSTOM_PERM")).build(); AppUser user = AppUser.builder().id(UUID.randomUUID()) - .username("custom").password("hashed").enabled(true) + .email("custom@example.com").password("hashed").enabled(true) .groups(Set.of(group)).build(); - when(userRepository.findByUsername("custom")).thenReturn(Optional.of(user)); + when(userRepository.findByEmail("custom@example.com")).thenReturn(Optional.of(user)); - UserDetails details = service.loadUserByUsername("custom"); + UserDetails details = service.loadUserByUsername("custom@example.com"); assertThat(details.getAuthorities()).extracting("authority") .contains("UNKNOWN_CUSTOM_PERM"); @@ -88,13 +86,13 @@ class CustomUserDetailsServiceTest { // ─── loadUserByUsername — disabled user ─────────────────────────────────── @Test - void loadUserByUsername_returnsDisabledUser_whenUserIsDisabled() { + void loadUserByEmail_returnsDisabledUser_whenUserIsDisabled() { AppUser user = AppUser.builder().id(UUID.randomUUID()) - .username("disabled").password("hashed").enabled(false) + .email("disabled@example.com").password("hashed").enabled(false) .groups(Set.of()).build(); - when(userRepository.findByUsername("disabled")).thenReturn(Optional.of(user)); + when(userRepository.findByEmail("disabled@example.com")).thenReturn(Optional.of(user)); - UserDetails details = service.loadUserByUsername("disabled"); + UserDetails details = service.loadUserByUsername("disabled@example.com"); assertThat(details.isEnabled()).isFalse(); } @@ -102,17 +100,17 @@ class CustomUserDetailsServiceTest { // ─── loadUserByUsername — multi-group permission merge ──────────────────── @Test - void loadUserByUsername_mergesPermissionsFromMultipleGroups() { + void loadUserByEmail_mergesPermissionsFromMultipleGroups() { UserGroup g1 = UserGroup.builder().id(UUID.randomUUID()).name("Readers") .permissions(Set.of("READ_ALL")).build(); UserGroup g2 = UserGroup.builder().id(UUID.randomUUID()).name("Writers") .permissions(Set.of("WRITE_ALL")).build(); AppUser user = AppUser.builder().id(UUID.randomUUID()) - .username("multi").password("hashed").enabled(true) + .email("multi@example.com").password("hashed").enabled(true) .groups(Set.of(g1, g2)).build(); - when(userRepository.findByUsername("multi")).thenReturn(Optional.of(user)); + when(userRepository.findByEmail("multi@example.com")).thenReturn(Optional.of(user)); - UserDetails details = service.loadUserByUsername("multi"); + UserDetails details = service.loadUserByUsername("multi@example.com"); assertThat(details.getAuthorities()).extracting("authority") .containsExactlyInAnyOrder("READ_ALL", "WRITE_ALL");