From 2cf492cd6c9c0e1e1521c60cb55912c3268b37d0 Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sat, 1 Nov 2025 08:47:02 +0100 Subject: [PATCH] feat(#2): Get notifications working again --- .../api/legalconsenthub.yml | 94 +++++- .../ApplicationFormService.kt | 39 ++- .../legalconsenthub/config/SecurityConfig.kt | 25 +- .../notification/Notification.kt | 6 +- .../notification/NotificationController.kt | 235 +++++++++----- .../notification/NotificationMapper.kt | 21 +- .../notification/NotificationRepository.kt | 164 +++++----- .../notification/NotificationService.kt | 290 ++++++++++-------- .../CustomJwtAuthenticationConverter.kt | 12 +- .../security/CustomJwtTokenPrincipal.kt | 2 +- legalconsenthub/app.vue | 8 +- .../components/NotificationsSlideover.vue | 17 +- .../notification/useNotification.ts | 35 ++- .../notification/useNotificationApi.ts | 27 +- legalconsenthub/layouts/default.vue | 8 +- legalconsenthub/server/api/[...].ts | 6 +- 16 files changed, 609 insertions(+), 380 deletions(-) diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml index 0b30a2e..f34bc54 100644 --- a/legalconsenthub-backend/api/legalconsenthub.yml +++ b/legalconsenthub-backend/api/legalconsenthub.yml @@ -609,6 +609,12 @@ paths: tags: - notification parameters: + - in: query + name: organizationId + required: true + schema: + type: string + description: Organization ID to get notifications for - in: query name: page schema: @@ -663,6 +669,13 @@ paths: operationId: getUnreadNotifications tags: - notification + parameters: + - in: query + name: organizationId + required: true + schema: + type: string + description: Organization ID to get unread notifications for responses: "200": description: List of unread notifications @@ -674,15 +687,30 @@ paths: $ref: "#/components/schemas/NotificationDto" "401": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "403": + description: User is not authorized to access this organization "500": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" /notifications/unread/count: get: - summary: Get count of unread notifications for the current user + summary: Get count of unread notifications for a user (public endpoint) operationId: getUnreadNotificationCount tags: - notification + parameters: + - in: query + name: userId + required: true + schema: + type: string + description: Keycloak user ID to get notification count for + - in: query + name: organizationId + required: true + schema: + type: string + description: Organization ID to get notification count for responses: "200": description: Count of unread notifications @@ -691,8 +719,6 @@ paths: schema: type: integer format: int64 - "401": - $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" "500": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" @@ -702,11 +728,43 @@ paths: operationId: markAllNotificationsAsRead tags: - notification + parameters: + - in: query + name: organizationId + required: true + schema: + type: string + description: Organization ID to mark notifications as read for responses: "204": description: All notifications marked as read "401": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "403": + description: User is not authorized to access this organization + "500": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" + + /notifications/clear-all: + delete: + summary: Clear all notifications for the current user + operationId: clearAllNotifications + tags: + - notification + parameters: + - in: query + name: organizationId + required: true + schema: + type: string + description: Organization ID to clear notifications for + responses: + "204": + description: All notifications cleared + "401": + $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "403": + description: User is not authorized to access this organization "500": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" @@ -723,6 +781,13 @@ paths: operationId: markNotificationAsRead tags: - notification + parameters: + - in: query + name: organizationId + required: true + schema: + type: string + description: Organization ID for authorization responses: "200": description: Notification marked as read @@ -734,6 +799,8 @@ paths: $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound" "401": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" + "403": + description: User is not authorized to access this organization "500": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" @@ -1130,7 +1197,6 @@ components: - message - clickTarget - isRead - - role - type - createdAt - organizationId @@ -1150,8 +1216,10 @@ components: nullable: true allOf: - $ref: "#/components/schemas/UserDto" - role: + targetRoles: type: string + description: Comma-separated list of target roles (only for role-based notifications) + nullable: true type: $ref: "#/components/schemas/NotificationType" organizationId: @@ -1166,7 +1234,6 @@ components: - title - message - clickTarget - - role - type - organizationId properties: @@ -1176,16 +1243,21 @@ components: type: string clickTarget: type: string - recipient: - nullable: true - allOf: - - $ref: "#/components/schemas/UserDto" - role: + recipientId: type: string + description: Keycloak ID of the recipient user. If not provided, notification will be role-based or organization-wide. + nullable: true + targetRoles: + type: array + items: + type: string + description: List of roles to send notification to. If both recipientId and targetRoles are null, notification will be sent to all organization members. + nullable: true type: $ref: "#/components/schemas/NotificationType" organizationId: type: string + description: The organization ID for this notification PagedNotificationDto: type: object diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt index 14497b5..30a0c42 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt @@ -5,20 +5,22 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedExc import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException +import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto +import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto +import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service import java.util.UUID -// import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService @Service class ApplicationFormService( private val applicationFormRepository: ApplicationFormRepository, private val applicationFormMapper: ApplicationFormMapper, - // private val notificationService: NotificationService + private val notificationService: NotificationService, ) { fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto) @@ -85,32 +87,29 @@ class ApplicationFormService( throw ApplicationFormNotUpdatedException(e, id) } - // Create notifications for relevant users - createSubmissionNotifications(savedApplicationForm) + createNotificationForOrganization(savedApplicationForm) return savedApplicationForm } - private fun createSubmissionNotifications(applicationForm: ApplicationForm) { + private fun createNotificationForOrganization(applicationForm: ApplicationForm) { val title = "Neuer Mitbestimmungsantrag eingereicht" val message = "Ein neuer Mitbestimmungsantrag '${applicationForm.name}' wurde von " + - "${applicationForm.createdBy.name} eingereicht und wartet auf Ihre Bearbeitung." + "${applicationForm.createdBy.name} eingereicht." val clickTarget = "/application-forms/${applicationForm.id}/0" - // // Create separate notification for each role that should be notified - // val rolesToNotify = listOf("admin", "works_council_member", "employer", "employee") - // - // rolesToNotify.forEach { role -> - // notificationService.createNotification( - // title = title, - // message = message, - // clickTarget = clickTarget, - // recipient = null, - // role = role, - // organizationId = applicationForm.organizationId, - // type = NotificationType.INFO - // ) - // } + val createNotificationDto = + CreateNotificationDto( + title = title, + message = message, + clickTarget = clickTarget, + recipientId = null, + targetRoles = null, + type = NotificationType.INFO, + organizationId = applicationForm.organizationId, + ) + + notificationService.createNotificationForOrganization(createNotificationDto) } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt index eb22060..8a062eb 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt @@ -3,6 +3,7 @@ package com.betriebsratkanzlei.legalconsenthub.config import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtAuthenticationConverter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order 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 @@ -14,16 +15,32 @@ import org.springframework.security.web.SecurityFilterChain @EnableMethodSecurity class SecurityConfig { @Bean - fun configure( + @Order(1) + fun publicFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher( + "/swagger-ui/**", + "/v3/**", + "/actuator/**", + "/notifications/unread/count", + ) + csrf { disable() } + authorizeHttpRequests { + authorize(anyRequest, permitAll) + } + } + return http.build() + } + + @Bean + @Order(2) + fun protectedFilterChain( http: HttpSecurity, customJwtAuthenticationConverter: CustomJwtAuthenticationConverter, ): SecurityFilterChain { http { csrf { disable() } authorizeHttpRequests { - authorize("/swagger-ui/**", permitAll) - authorize("/v3/**", permitAll) - authorize("/actuator/**", permitAll) authorize(anyRequest, authenticated) } oauth2ResourceServer { diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/Notification.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/Notification.kt index 985314f..ae4de71 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/Notification.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/Notification.kt @@ -32,9 +32,9 @@ class Notification( var isRead: Boolean = false, @ManyToOne @JoinColumn(name = "recipient_id", nullable = true) - var recipient: User?, - @Column(nullable = false) - var role: String = "", + var recipient: User? = null, + @Column(nullable = true, columnDefinition = "TEXT") + var targetRoles: String? = null, @Enumerated(EnumType.STRING) @Column(nullable = false) var type: NotificationType = NotificationType.INFO, diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt index a3b43d8..bcf4a5a 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt @@ -1,83 +1,152 @@ -// package com.betriebsratkanzlei.legalconsenthub.notification -// -// import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal -// import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi -// import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto -// import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto -// import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto -// import org.springframework.http.ResponseEntity -// import org.springframework.security.core.context.SecurityContextHolder -// import org.springframework.web.bind.annotation.RestController -// import java.util.UUID -// -// @RestController -// class NotificationController( -// private val notificationService: NotificationService, -// private val notificationMapper: NotificationMapper, -// private val pagedNotificationMapper: PagedNotificationMapper -// ) : NotificationApi { -// -// override fun getNotifications(page: Int, size: Int): ResponseEntity { -// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal -// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") -// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") -// -// val notifications = notificationService.getNotifications( -// recipientId = recipientId, -// organizationId = organizationId, -// page = page, -// size = size -// ) -// -// return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications)) -// } -// -// override fun getUnreadNotifications(): ResponseEntity> { -// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal -// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") -// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") -// -// val notifications = notificationService.getUnreadNotifications( -// recipientId = recipientId, -// organizationId = organizationId -// ) -// -// return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) }) -// } -// -// override fun getUnreadNotificationCount(): ResponseEntity { -// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal -// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") -// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") -// -// val count = notificationService.getUnreadNotificationCount( -// recipientId = recipientId, -// organizationId = organizationId -// ) -// -// return ResponseEntity.ok(count) -// } -// -// override fun markAllNotificationsAsRead(): ResponseEntity { -// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal -// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") -// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") -// -// notificationService.markAllAsRead( -// recipientId = recipientId, -// organizationId = organizationId -// ) -// -// return ResponseEntity.noContent().build() -// } -// -// override fun markNotificationAsRead(id: UUID): ResponseEntity { -// val notification = notificationService.markNotificationAsRead(id) -// return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) -// } -// -// override fun createNotification(createNotificationDto: CreateNotificationDto): ResponseEntity { -// val notification = notificationService.createNotification(createNotificationDto) -// return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) -// } -// } +package com.betriebsratkanzlei.legalconsenthub.notification + +import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal +import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi +import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto +import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto +import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto +import org.springframework.http.ResponseEntity +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +class NotificationController( + private val notificationService: NotificationService, + private val notificationMapper: NotificationMapper, + private val pagedNotificationMapper: PagedNotificationMapper, +) : NotificationApi { + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')", + ) + override fun getNotifications( + organizationId: String, + page: Int, + size: Int, + ): ResponseEntity { + val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal + val recipientId = principal.id ?: throw IllegalStateException("User ID not found") + validateOrganizationAccess(principal, organizationId) + val userRoles = principal.roles + + val notifications = + notificationService.getNotifications( + recipientKeycloakId = recipientId, + organizationId = organizationId, + userRoles = userRoles, + page = page, + size = size, + ) + + return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications)) + } + + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')", + ) + override fun getUnreadNotifications(organizationId: String): ResponseEntity> { + val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal + val recipientId = principal.id ?: throw IllegalStateException("User ID not found") + validateOrganizationAccess(principal, organizationId) + val userRoles = principal.roles + + val notifications = + notificationService.getUnreadNotifications( + recipientKeycloakId = recipientId, + organizationId = organizationId, + userRoles = userRoles, + ) + + return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) }) + } + + override fun getUnreadNotificationCount( + userId: String, + organizationId: String, + ): ResponseEntity { + val count = + notificationService.getUnreadNotificationCount( + recipientKeycloakId = userId, + organizationId = organizationId, + userRoles = emptyList(), + ) + + return ResponseEntity.ok(count) + } + + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')", + ) + override fun markAllNotificationsAsRead(organizationId: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal + val recipientId = principal.id ?: throw IllegalStateException("User ID not found") + validateOrganizationAccess(principal, organizationId) + + notificationService.markAllAsRead( + recipientKeycloakId = recipientId, + organizationId = organizationId, + ) + + return ResponseEntity.noContent().build() + } + + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')", + ) + override fun markNotificationAsRead( + id: UUID, + organizationId: String, + ): ResponseEntity { + val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal + validateOrganizationAccess(principal, organizationId) + + val notification = notificationService.markNotificationAsRead(id) + return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) + } + + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL')", + ) + override fun createNotification(createNotificationDto: CreateNotificationDto): ResponseEntity { + val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal + validateOrganizationAccess(principal, createNotificationDto.organizationId) + + val notification = + when { + createNotificationDto.recipientId != null -> + notificationService.createNotificationForUser(createNotificationDto) + !createNotificationDto.targetRoles.isNullOrEmpty() -> + notificationService.createNotificationForRoles(createNotificationDto) + else -> + notificationService.createNotificationForOrganization(createNotificationDto) + } + + return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) + } + + @PreAuthorize( + "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')", + ) + override fun clearAllNotifications(organizationId: String): ResponseEntity { + val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal + val recipientId = principal.id ?: throw IllegalStateException("User ID not found") + validateOrganizationAccess(principal, organizationId) + + notificationService.clearAllNotifications( + recipientKeycloakId = recipientId, + organizationId = organizationId, + ) + + return ResponseEntity.noContent().build() + } + + private fun validateOrganizationAccess( + principal: CustomJwtTokenPrincipal, + organizationId: String, + ) { + if (organizationId !in principal.organizationIds) { + throw SecurityException("User is not authorized to access organization: $organizationId") + } + } +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationMapper.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationMapper.kt index 039aa93..9102835 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationMapper.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationMapper.kt @@ -1,5 +1,6 @@ package com.betriebsratkanzlei.legalconsenthub.notification +import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.user.UserMapper import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto @@ -17,21 +18,29 @@ class NotificationMapper( clickTarget = notification.clickTarget, isRead = notification.isRead, recipient = notification.recipient?.let { userMapper.toUserDto(it) }, - role = notification.role, type = notification.type, organizationId = notification.organizationId, createdAt = notification.createdAt!!, + targetRoles = notification.targetRoles, ) - fun toNotification(createNotificationDto: CreateNotificationDto): Notification = - Notification( + fun toNotification( + createNotificationDto: CreateNotificationDto, + recipient: User? = null, + targetRoles: String? = null, + ): Notification { + if (recipient != null && targetRoles != null) { + throw IllegalArgumentException("Only one of recipient or targetRoles can be provided") + } + + return Notification( title = createNotificationDto.title, message = createNotificationDto.message, clickTarget = createNotificationDto.clickTarget, - isRead = false, - recipient = createNotificationDto.recipient?.let { userMapper.toUser(it) }, - role = createNotificationDto.role, + recipient = recipient, + targetRoles = targetRoles, type = createNotificationDto.type, organizationId = createNotificationDto.organizationId, ) + } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt index eb13f57..2ad915f 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt @@ -1,84 +1,80 @@ -// package com.betriebsratkanzlei.legalconsenthub.notification -// -// import org.springframework.data.domain.Page -// import org.springframework.data.domain.Pageable -// import org.springframework.data.jpa.repository.JpaRepository -// import org.springframework.data.jpa.repository.Modifying -// import org.springframework.data.jpa.repository.Query -// import org.springframework.data.repository.query.Param -// import org.springframework.stereotype.Repository -// import java.util.UUID -// -// @Repository -// interface NotificationRepository : JpaRepository { -// -// fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page -// fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List -// fun countByRecipientIdAndIsReadFalse(recipientId: String?): Long -// -// @Query( -// """ -// SELECT n FROM Notification n WHERE -// (n.recipient.keycloakId = :recipientId) OR -// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR -// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) -// ORDER BY n.createdAt DESC -// """ -// ) -// fun findUserNotificationsByOrgRole( -// @Param("recipientId") recipientId: String, -// @Param("organizationIds") organizationIds: List, -// @Param("orgRolePairs") orgRolePairs: List, -// pageable: Pageable -// ): Page -// -// @Query( -// """ -// SELECT n FROM Notification n WHERE -// ((n.recipient.keycloakId = :recipientId) OR -// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR -// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) -// AND n.isRead = false -// ORDER BY n.createdAt DESC -// """ -// ) -// fun findUnreadUserNotificationsByOrgRole( -// @Param("recipientId") recipientId: String, -// @Param("organizationIds") organizationIds: List, -// @Param("orgRolePairs") orgRolePairs: List -// ): List -// -// @Query( -// """ -// SELECT COUNT(n) FROM Notification n WHERE -// ((n.recipient.keycloakId = :recipientId) OR -// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR -// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) -// AND n.isRead = false -// """ -// ) -// fun countUnreadUserNotifications( -// @Param("recipientId") recipientId: String, -// @Param("organizationIds") organizationIds: List, -// @Param("orgRolePairs") orgRolePairs: List -// ): Long -// -// @Modifying -// @Query( -// """ -// UPDATE Notification n SET n.isRead = true WHERE -// (n.recipient.keycloakId = :recipientId) OR -// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR -// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) -// """ -// ) -// fun markAllUserNotificationsAsRead( -// @Param("recipientId") recipientId: String, -// @Param("organizationIds") organizationIds: List, -// @Param("orgRolePairs") orgRolePairs: List -// ) -// -// @Modifying -// @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.keycloakId = :recipientId") -// fun markAllAsReadByRecipientId(@Param("recipientId") recipientId: String) -// } +package com.betriebsratkanzlei.legalconsenthub.notification + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Modifying +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface NotificationRepository : JpaRepository { + @Query( + """ + SELECT n FROM Notification n + WHERE n.organizationId = :organizationId + AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL) + ORDER BY n.createdAt DESC + """, + ) + fun findByRecipientOrWithoutRecipientAndOrganizationId( + @Param("keycloakId") keycloakId: String, + @Param("organizationId") organizationId: String, + pageable: Pageable, + ): Page + + @Query( + """ + SELECT n FROM Notification n + WHERE n.organizationId = :organizationId + AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL) + AND n.isRead = false + ORDER BY n.createdAt DESC + """, + ) + fun findUnreadByRecipientOrWithoutRecipientAndOrganizationId( + @Param("keycloakId") keycloakId: String, + @Param("organizationId") organizationId: String, + ): List + + @Query( + """ + SELECT COUNT(n) FROM Notification n + WHERE n.organizationId = :organizationId + AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL) + AND n.isRead = false + """, + ) + fun countUnreadByRecipientOrWithoutRecipientAndOrganizationId( + @Param("keycloakId") keycloakId: String, + @Param("organizationId") organizationId: String, + ): Long + + @Modifying + @Query( + """ + UPDATE Notification n SET n.isRead = true + WHERE n.organizationId = :organizationId + AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL) + """, + ) + fun markAllAsReadByRecipientAndOrganization( + @Param("keycloakId") keycloakId: String, + @Param("organizationId") organizationId: String, + ) + + @Modifying + @Query( + """ + DELETE FROM Notification n + WHERE n.organizationId = :organizationId + AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL) + """, + ) + fun deleteAllByRecipientAndOrganization( + @Param("keycloakId") keycloakId: String, + @Param("organizationId") organizationId: String, + ) +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt index 2e0aeb0..95fe1dd 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt @@ -1,127 +1,163 @@ -// package com.betriebsratkanzlei.legalconsenthub.notification -// -// import com.betriebsratkanzlei.legalconsenthub.user.User -// import com.betriebsratkanzlei.legalconsenthub.user.UserRepository -// import com.betriebsratkanzlei.legalconsenthub.user.UserRoleConverter -// import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto -// import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType -// import org.springframework.data.domain.Page -// import org.springframework.data.domain.PageRequest -// import org.springframework.stereotype.Service -// import org.springframework.transaction.annotation.Transactional -// import java.util.UUID -// -// @Service -// class NotificationService( -// private val notificationRepository: NotificationRepository, -// private val notificationMapper: NotificationMapper, -// private val userRepository: UserRepository, -// private val userRoleConverter: UserRoleConverter -// ) { -// -// fun createNotification(createNotificationDto: CreateNotificationDto): Notification { -// val notification = notificationMapper.toNotification(createNotificationDto) -// return notificationRepository.save(notification) -// } -// -// fun createNotification( -// title: String, -// message: String, -// clickTarget: String, -// recipient: User?, -// role: String, -// organizationId: String, -// type: NotificationType = NotificationType.INFO -// ): Notification { -// val notification = Notification( -// title = title, -// message = message, -// clickTarget = clickTarget, -// recipient = recipient, -// role = role, -// type = type, -// organizationId = organizationId -// ) -// return notificationRepository.save(notification) -// } -// -// fun getNotifications( -// recipientId: String, -// organizationId: String, -// page: Int = 0, -// size: Int = 20 -// ): Page { -// val user = userRepository.findById(recipientId) -// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } -// -// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) -// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } -// -// val pageable = PageRequest.of(page, size) -// return if (userRoles.isNotEmpty()) { -// notificationRepository.findUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs, pageable) -// } else { -// notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, pageable) -// } -// } -// -// fun getUnreadNotifications( -// recipientId: String, -// organizationId: String -// ): List { -// val user = userRepository.findById(recipientId) -// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } -// -// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) -// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } -// -// return if (userRoles.isNotEmpty()) { -// notificationRepository.findUnreadUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs) -// } else { -// notificationRepository.findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId) -// } -// } -// -// fun getUnreadNotificationCount( -// recipientId: String, -// organizationId: String -// ): Long { -// val user = userRepository.findById(recipientId) -// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } -// -// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) -// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } -// -// return if (userRoles.isNotEmpty()) { -// notificationRepository.countUnreadUserNotifications(recipientId, listOf(organizationId), orgRolePairs) -// } else { -// notificationRepository.countByRecipientIdAndIsReadFalse(recipientId) -// } -// } -// -// @Transactional -// fun markAllAsRead( -// recipientId: String, -// organizationId: String -// ) { -// val user = userRepository.findById(recipientId) -// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } -// -// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) -// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } -// -// if (userRoles.isNotEmpty()) { -// notificationRepository.markAllUserNotificationsAsRead(recipientId, listOf(organizationId), orgRolePairs) -// } else { -// notificationRepository.markAllAsReadByRecipientId(recipientId) -// } -// } -// -// @Transactional -// fun markNotificationAsRead(notificationId: UUID): Notification { -// val notification = notificationRepository.findById(notificationId) -// .orElseThrow { IllegalArgumentException("Notification not found with id: $notificationId") } -// notification.isRead = true -// return notificationRepository.save(notification) -// } -// } +package com.betriebsratkanzlei.legalconsenthub.notification + +import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal +import com.betriebsratkanzlei.legalconsenthub.user.UserRepository +import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.UUID + +@Service +class NotificationService( + private val notificationRepository: NotificationRepository, + private val userRepository: UserRepository, + private val notificationMapper: NotificationMapper, +) { + fun createNotificationForUser(createNotificationDto: CreateNotificationDto): Notification { + val recipientKeycloakId = + createNotificationDto.recipientId + ?: throw IllegalArgumentException("recipientId must be provided for user notifications") + + val recipient = + userRepository + .findById(recipientKeycloakId) + .orElseThrow { IllegalArgumentException("User not found with id: $recipientKeycloakId") } + + val notification = + notificationMapper.toNotification( + createNotificationDto = createNotificationDto, + recipient = recipient, + ) + return notificationRepository.save(notification) + } + + @Transactional + fun createNotificationForRoles(createNotificationDto: CreateNotificationDto): Notification { + val targetRoles = + createNotificationDto.targetRoles + ?: throw IllegalArgumentException("targetRoles must be provided for role-based notifications") + + val notification = + notificationMapper.toNotification( + createNotificationDto = createNotificationDto, + targetRoles = targetRoles.joinToString(","), + ) + + return notificationRepository.save(notification) + } + + @Transactional + fun createNotificationForOrganization(createNotificationDto: CreateNotificationDto): Notification { + val notification = + notificationMapper.toNotification( + createNotificationDto = createNotificationDto, + ) + + return notificationRepository.save(notification) + } + + fun getNotifications( + recipientKeycloakId: String, + organizationId: String, + userRoles: List, + page: Int = 0, + size: Int = 20, + ): Page { + val pageable = PageRequest.of(page, size) + val allNotifications = + notificationRepository.findByRecipientOrWithoutRecipientAndOrganizationId( + recipientKeycloakId, + organizationId, + pageable, + ) + + val filteredContent = + allNotifications.content.filter { notification -> + when { + // Direct recipient notification + notification.recipient != null -> true + // Organization-wide notification (no recipient, no roles) + notification.targetRoles.isNullOrEmpty() -> true + // Role-based notification + else -> { + val targetRolesList = notification.targetRoles!!.split(",") + userRoles.any { it in targetRolesList } + } + } + } + + return PageImpl(filteredContent, pageable, allNotifications.totalElements) + } + + fun getUnreadNotifications( + recipientKeycloakId: String, + organizationId: String, + userRoles: List, + ): List { + val allNotifications = + notificationRepository.findUnreadByRecipientOrWithoutRecipientAndOrganizationId( + recipientKeycloakId, + organizationId, + ) + + return allNotifications.filter { notification -> + when { + // Direct recipient notification + notification.recipient != null -> true + // Organization-wide notification (no recipient, no roles) + notification.targetRoles.isNullOrEmpty() -> true + // Role-based notification + else -> { + val targetRolesList = notification.targetRoles!!.split(",") + userRoles.any { it in targetRolesList } + } + } + } + } + + fun getUnreadNotificationCount( + recipientKeycloakId: String, + organizationId: String, + userRoles: List, + ): Long = getUnreadNotifications(recipientKeycloakId, organizationId, userRoles).size.toLong() + + @Transactional + fun markAllAsRead( + recipientKeycloakId: String, + organizationId: String, + ) { + notificationRepository.markAllAsReadByRecipientAndOrganization(recipientKeycloakId, organizationId) + } + + @Transactional + fun markNotificationAsRead(notificationId: UUID): Notification { + val notification = + notificationRepository + .findById(notificationId) + .orElseThrow { IllegalArgumentException("Notification not found with id: $notificationId") } + + val principal = SecurityContextHolder.getContext().authentication.principal as? CustomJwtTokenPrincipal + val currentUserKeycloakId = + principal?.id + ?: throw IllegalStateException("User ID not found in security context") + + if (notification.recipient != null && notification.recipient?.keycloakId != currentUserKeycloakId) { + throw IllegalArgumentException("Cannot mark notification as read for another user") + } + + notification.isRead = true + return notificationRepository.save(notification) + } + + @Transactional + fun clearAllNotifications( + recipientKeycloakId: String, + organizationId: String, + ) { + notificationRepository.deleteAllByRecipientAndOrganization(recipientKeycloakId, organizationId) + } +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt index 898c896..6ce4027 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt @@ -17,13 +17,23 @@ class CustomJwtAuthenticationConverter : Converter val roles = (legalconsenthubResource?.get("roles") as? List<*>)?.mapNotNull { it as? String } ?: emptyList() + val organizationIds = extractOrganizationIds(jwt) + val authorities: Collection = roles.map { role -> SimpleGrantedAuthority("ROLE_$role") } - val principal = CustomJwtTokenPrincipal(userId, username, roles) + val principal = CustomJwtTokenPrincipal(userId, username, roles, organizationIds) return CustomJwtAuthentication(jwt, principal, authorities) } + + private fun extractOrganizationIds(jwt: Jwt): List { + val organizationClaim = jwt.getClaimAsMap("organization") ?: return emptyList() + + return organizationClaim.values.mapNotNull { meta -> + (meta as? Map<*, *>)?.get("id") as? String + } + } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt index 62837f6..38da47a 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt @@ -4,5 +4,5 @@ data class CustomJwtTokenPrincipal( val id: String? = null, val name: String? = null, val roles: List = emptyList(), - val organizationId: String? = null, + val organizationIds: List = emptyList(), ) diff --git a/legalconsenthub/app.vue b/legalconsenthub/app.vue index 1d606f5..57b60a6 100644 --- a/legalconsenthub/app.vue +++ b/legalconsenthub/app.vue @@ -18,8 +18,8 @@ const color = computed(() => (colorMode.value === 'dark' ? '#111827' : 'white')) useHead({ meta: [ { charset: 'utf-8' }, - { userName: 'viewport', content: 'width=device-width, initial-scale=1' }, - { key: 'theme-color', userName: 'theme-color', content: color } + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { key: 'theme-color', name: 'theme-color', content: color } ], link: [{ rel: 'icon', href: '/favicon.ico' }], htmlAttrs: { @@ -39,8 +39,4 @@ useSeoMeta({ twitterImage: 'https://dashboard-template.nuxt.dev/social-card.png', twitterCard: 'summary_large_image' }) - -// onBeforeMount(() => { -// $fetch('/api/auth/refresh') -// }) diff --git a/legalconsenthub/components/NotificationsSlideover.vue b/legalconsenthub/components/NotificationsSlideover.vue index 1a34b5a..6d7f9d4 100644 --- a/legalconsenthub/components/NotificationsSlideover.vue +++ b/legalconsenthub/components/NotificationsSlideover.vue @@ -53,9 +53,6 @@ > {{ notification.type }} - - {{ notification.role }} - @@ -80,13 +77,13 @@ const isOpen = computed({ set: (value) => emit('update:modelValue', value) }) -// const { notifications, fetchNotifications, handleNotificationClick } = useNotification() -// -// watch(isOpen, async (newValue) => { -// if (newValue) { -// await fetchNotifications() -// } -// }) +const { notifications, fetchNotifications, handleNotificationClick } = useNotification() + +watch(isOpen, async (newValue) => { + if (newValue) { + await fetchNotifications() + } +}) function onNotificationClick(notification: NotificationDto) { handleNotificationClick(notification) diff --git a/legalconsenthub/composables/notification/useNotification.ts b/legalconsenthub/composables/notification/useNotification.ts index 8549609..0466e33 100644 --- a/legalconsenthub/composables/notification/useNotification.ts +++ b/legalconsenthub/composables/notification/useNotification.ts @@ -9,15 +9,24 @@ export const useNotification = () => { markNotificationAsRead } = useNotificationApi() + const userStore = useUserStore() + const organizationId = computed(() => userStore.selectedOrganization?.id) + const { user } = useUserSession() + const userId = computed(() => user.value?.keycloakId) + const notifications = ref([]) const unreadNotifications = ref([]) const unreadCount = ref(0) const isLoading = ref(false) const fetchNotifications = async (page: number = 0, size: number = 20) => { + if (!organizationId.value) { + console.warn('No organization selected') + return + } isLoading.value = true try { - const response = await getNotifications(page, size) + const response = await getNotifications(organizationId.value, page, size) notifications.value = response.content || [] return response } catch (error) { @@ -29,8 +38,12 @@ export const useNotification = () => { } const fetchUnreadNotifications = async () => { + if (!organizationId.value) { + console.warn('No organization selected') + return + } try { - const response = await getUnreadNotifications() + const response = await getUnreadNotifications(organizationId.value) unreadNotifications.value = response || [] return response } catch (error) { @@ -40,8 +53,12 @@ export const useNotification = () => { } const fetchUnreadCount = async () => { + if (!userId.value || !organizationId.value) { + console.warn('No user or organization selected') + return + } try { - const count = await getUnreadNotificationCount() + const count = await getUnreadNotificationCount(userId.value, organizationId.value) unreadCount.value = count || 0 return count } catch (error) { @@ -51,8 +68,12 @@ export const useNotification = () => { } const markAllAsRead = async () => { + if (!organizationId.value) { + console.warn('No organization selected') + return + } try { - await markAllNotificationsAsRead() + await markAllNotificationsAsRead(organizationId.value) unreadCount.value = 0 unreadNotifications.value = [] notifications.value = notifications.value.map((n) => ({ ...n, isRead: true })) @@ -63,8 +84,12 @@ export const useNotification = () => { } const markAsRead = async (notificationId: string) => { + if (!organizationId.value) { + console.warn('No organization selected') + return + } try { - await markNotificationAsRead(notificationId) + await markNotificationAsRead(notificationId, organizationId.value) const index = notifications.value.findIndex((n) => n.id === notificationId) if (index !== -1) { notifications.value[index].isRead = true diff --git a/legalconsenthub/composables/notification/useNotificationApi.ts b/legalconsenthub/composables/notification/useNotificationApi.ts index 97dca0b..49bcd63 100644 --- a/legalconsenthub/composables/notification/useNotificationApi.ts +++ b/legalconsenthub/composables/notification/useNotificationApi.ts @@ -28,24 +28,28 @@ export function useNotificationApi() { return notificationApiClient.createNotification({ createNotificationDto }) } - async function getNotifications(page?: number, size?: number): Promise { - return notificationApiClient.getNotifications({ page, size }) + async function getNotifications(organizationId: string, page?: number, size?: number): Promise { + return notificationApiClient.getNotifications({ organizationId, page, size }) } - async function getUnreadNotifications(): Promise { - return notificationApiClient.getUnreadNotifications() + async function getUnreadNotifications(organizationId: string): Promise { + return notificationApiClient.getUnreadNotifications({ organizationId }) } - async function getUnreadNotificationCount(): Promise { - return notificationApiClient.getUnreadNotificationCount() + async function getUnreadNotificationCount(userId: string, organizationId: string): Promise { + return notificationApiClient.getUnreadNotificationCount({ userId, organizationId }) } - async function markAllNotificationsAsRead(): Promise { - return notificationApiClient.markAllNotificationsAsRead() + async function markAllNotificationsAsRead(organizationId: string): Promise { + return notificationApiClient.markAllNotificationsAsRead({ organizationId }) } - async function markNotificationAsRead(id: string): Promise { - return notificationApiClient.markNotificationAsRead({ id }) + async function markNotificationAsRead(id: string, organizationId: string): Promise { + return notificationApiClient.markNotificationAsRead({ id, organizationId }) + } + + async function clearAllNotifications(organizationId: string): Promise { + return notificationApiClient.clearAllNotifications({ organizationId }) } return { @@ -54,6 +58,7 @@ export function useNotificationApi() { getUnreadNotifications, getUnreadNotificationCount, markAllNotificationsAsRead, - markNotificationAsRead + markNotificationAsRead, + clearAllNotifications } } diff --git a/legalconsenthub/layouts/default.vue b/legalconsenthub/layouts/default.vue index dd548c6..8f1ed9b 100644 --- a/legalconsenthub/layouts/default.vue +++ b/legalconsenthub/layouts/default.vue @@ -42,10 +42,10 @@ const open = ref(false) const isNotificationsSlideoverOpen = ref(false) const { unreadCount, fetchUnreadCount, startPeriodicRefresh } = useNotification() -// onMounted(async () => { -// await fetchUnreadCount() -// startPeriodicRefresh() -// }) +onMounted(async () => { + await fetchUnreadCount() + startPeriodicRefresh() +}) provide('notificationState', { isNotificationsSlideoverOpen, diff --git a/legalconsenthub/server/api/[...].ts b/legalconsenthub/server/api/[...].ts index 7e6680f..56fb23c 100644 --- a/legalconsenthub/server/api/[...].ts +++ b/legalconsenthub/server/api/[...].ts @@ -12,9 +12,6 @@ export default defineEventHandler(async (event: H3Event) => { const session = await getUserSession(event) const accessToken = session?.jwt?.accessToken - console.log('🔍 PROXY: proxying request, found access token:', accessToken) - console.log('🔍 PROXY: Expiration:', new Date(jwtDecode(accessToken).exp! * 1000).toISOString()) - if (!accessToken) { throw createError({ statusCode: 401, @@ -22,7 +19,8 @@ export default defineEventHandler(async (event: H3Event) => { }) } - console.log('🔀 proxying request to', target) + console.log('🔀 [PROXY] Expiration:', new Date(jwtDecode(accessToken).exp! * 1000).toISOString()) + console.log('🔀 [PROXY] Proxying request to:', target) return proxyRequest(event, target, { headers: {