From d13422c65adefb32b404b96f92a6a2e3d44d57fb Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 28 Mar 2026 11:44:17 +0100 Subject: [PATCH] fix(#71,#73): remove class-level permission gate and add annotationId to notifications - Remove @RequirePermission(READ_ALL) from NotificationController class level so authenticated users with any permission (or none) can access their own notifications - Add V19 migration, annotationId field to Notification entity and NotificationDTO - NotificationService now stores annotationId from comment on both REPLY and MENTION - Update controller tests: permission tests now expect 200, DTO constructor includes annotationId Co-Authored-By: Claude Sonnet 4.6 --- .../controller/NotificationController.java | 1 - .../familienarchiv/dto/NotificationDTO.java | 1 + .../familienarchiv/model/Notification.java | 3 ++ .../service/NotificationService.java | 3 ++ ...19__add_annotation_id_to_notifications.sql | 1 + .../NotificationControllerTest.java | 20 +++++++++--- frontend/src/routes/profile/+page.svelte | 31 ++++++++++++++----- 7 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V19__add_annotation_id_to_notifications.sql diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/NotificationController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/NotificationController.java index 1e2ddd63..1a572403 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/NotificationController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/NotificationController.java @@ -19,7 +19,6 @@ import java.util.UUID; @RestController @RequiredArgsConstructor -@RequirePermission(Permission.READ_ALL) public class NotificationController { private final NotificationService notificationService; diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/NotificationDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/NotificationDTO.java index 26c91db8..cbe885ba 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/dto/NotificationDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/NotificationDTO.java @@ -10,6 +10,7 @@ public record NotificationDTO( NotificationType type, UUID documentId, UUID referenceId, + UUID annotationId, boolean read, LocalDateTime createdAt, String actorName diff --git a/backend/src/main/java/org/raddatz/familienarchiv/model/Notification.java b/backend/src/main/java/org/raddatz/familienarchiv/model/Notification.java index e415e574..ee5d3b9e 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/model/Notification.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/model/Notification.java @@ -37,6 +37,9 @@ public class Notification { @Column(name = "reference_id") private UUID referenceId; + @Column(name = "annotation_id") + private UUID annotationId; + @Column(nullable = false) @Builder.Default @Schema(requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/NotificationService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/NotificationService.java index d6d371a0..cee737c1 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/NotificationService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/NotificationService.java @@ -55,6 +55,7 @@ public class NotificationService { .type(NotificationType.REPLY) .documentId(reply.getDocumentId()) .referenceId(reply.getId()) + .annotationId(reply.getAnnotationId()) .actorName(reply.getAuthorName()) .build(); notificationRepository.save(notification); @@ -80,6 +81,7 @@ public class NotificationService { .type(NotificationType.MENTION) .documentId(comment.getDocumentId()) .referenceId(comment.getId()) + .annotationId(comment.getAnnotationId()) .actorName(comment.getAuthorName()) .build(); notificationRepository.save(notification); @@ -129,6 +131,7 @@ public class NotificationService { n.getType(), n.getDocumentId(), n.getReferenceId(), + n.getAnnotationId(), n.isRead(), n.getCreatedAt(), n.getActorName() diff --git a/backend/src/main/resources/db/migration/V19__add_annotation_id_to_notifications.sql b/backend/src/main/resources/db/migration/V19__add_annotation_id_to_notifications.sql new file mode 100644 index 00000000..67e4d823 --- /dev/null +++ b/backend/src/main/resources/db/migration/V19__add_annotation_id_to_notifications.sql @@ -0,0 +1 @@ +ALTER TABLE notifications ADD COLUMN annotation_id UUID; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/NotificationControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/NotificationControllerTest.java index 6c988d74..14c0590a 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/NotificationControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/NotificationControllerTest.java @@ -53,9 +53,14 @@ class NotificationControllerTest { @Test @WithMockUser(username = "testuser") - void getNotifications_returns403_whenUserLacksPermission() throws Exception { + void getNotifications_returns200_whenAuthenticatedWithNoPermissions() throws Exception { + AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); + when(userService.findByUsername("testuser")).thenReturn(user); + when(notificationService.getNotifications(eq(USER_ID), any())) + .thenReturn(new PageImpl<>(List.of())); + mockMvc.perform(get("/api/notifications")) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()); } @Test @@ -64,7 +69,7 @@ class NotificationControllerTest { AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); NotificationDTO dto = new NotificationDTO( UUID.randomUUID(), NotificationType.REPLY, UUID.randomUUID(), - UUID.randomUUID(), false, LocalDateTime.now(), "Anna Smith"); + UUID.randomUUID(), null, false, LocalDateTime.now(), "Anna Smith"); when(userService.findByUsername("testuser")).thenReturn(user); when(notificationService.getNotifications(eq(USER_ID), any())) @@ -185,9 +190,14 @@ class NotificationControllerTest { @Test @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) - void getNotifications_returns403_whenUserHasOnlyWriteAll() throws Exception { + void getNotifications_returns200_whenUserHasOnlyWriteAll() throws Exception { + AppUser user = AppUser.builder().id(USER_ID).username("testuser").build(); + when(userService.findByUsername("testuser")).thenReturn(user); + when(notificationService.getNotifications(eq(USER_ID), any())) + .thenReturn(new PageImpl<>(List.of())); + mockMvc.perform(get("/api/notifications")) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()); } // ─── PUT /api/users/me/notification-preferences ────────────────────────── diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte index 1113e783..547b6603 100644 --- a/frontend/src/routes/profile/+page.svelte +++ b/frontend/src/routes/profile/+page.svelte @@ -1,14 +1,14 @@
@@ -53,32 +53,49 @@ let notifyOnMention = $state(untrack(() => data.notificationPrefs?.notifyOnMenti
{/if} -
+ async ({ update }) => update({ reset: false })} + >
-
+ {#if !hasEmail} +

+ {m.notification_prefs_no_email()} +

+ {/if} +