feat(#2): Get notifications working again

This commit is contained in:
2025-11-01 08:47:02 +01:00
parent 841341857d
commit 2cf492cd6c
16 changed files with 609 additions and 380 deletions

View File

@@ -609,6 +609,12 @@ paths:
tags: tags:
- notification - notification
parameters: parameters:
- in: query
name: organizationId
required: true
schema:
type: string
description: Organization ID to get notifications for
- in: query - in: query
name: page name: page
schema: schema:
@@ -663,6 +669,13 @@ paths:
operationId: getUnreadNotifications operationId: getUnreadNotifications
tags: tags:
- notification - notification
parameters:
- in: query
name: organizationId
required: true
schema:
type: string
description: Organization ID to get unread notifications for
responses: responses:
"200": "200":
description: List of unread notifications description: List of unread notifications
@@ -674,15 +687,30 @@ paths:
$ref: "#/components/schemas/NotificationDto" $ref: "#/components/schemas/NotificationDto"
"401": "401":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" $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": "500":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
/notifications/unread/count: /notifications/unread/count:
get: get:
summary: Get count of unread notifications for the current user summary: Get count of unread notifications for a user (public endpoint)
operationId: getUnreadNotificationCount operationId: getUnreadNotificationCount
tags: tags:
- notification - 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: responses:
"200": "200":
description: Count of unread notifications description: Count of unread notifications
@@ -691,8 +719,6 @@ paths:
schema: schema:
type: integer type: integer
format: int64 format: int64
"401":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
"500": "500":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
@@ -702,11 +728,43 @@ paths:
operationId: markAllNotificationsAsRead operationId: markAllNotificationsAsRead
tags: tags:
- notification - notification
parameters:
- in: query
name: organizationId
required: true
schema:
type: string
description: Organization ID to mark notifications as read for
responses: responses:
"204": "204":
description: All notifications marked as read description: All notifications marked as read
"401": "401":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" $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": "500":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
@@ -723,6 +781,13 @@ paths:
operationId: markNotificationAsRead operationId: markNotificationAsRead
tags: tags:
- notification - notification
parameters:
- in: query
name: organizationId
required: true
schema:
type: string
description: Organization ID for authorization
responses: responses:
"200": "200":
description: Notification marked as read 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" $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound"
"401": "401":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" $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": "500":
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
@@ -1130,7 +1197,6 @@ components:
- message - message
- clickTarget - clickTarget
- isRead - isRead
- role
- type - type
- createdAt - createdAt
- organizationId - organizationId
@@ -1150,8 +1216,10 @@ components:
nullable: true nullable: true
allOf: allOf:
- $ref: "#/components/schemas/UserDto" - $ref: "#/components/schemas/UserDto"
role: targetRoles:
type: string type: string
description: Comma-separated list of target roles (only for role-based notifications)
nullable: true
type: type:
$ref: "#/components/schemas/NotificationType" $ref: "#/components/schemas/NotificationType"
organizationId: organizationId:
@@ -1166,7 +1234,6 @@ components:
- title - title
- message - message
- clickTarget - clickTarget
- role
- type - type
- organizationId - organizationId
properties: properties:
@@ -1176,16 +1243,21 @@ components:
type: string type: string
clickTarget: clickTarget:
type: string type: string
recipient: recipientId:
nullable: true
allOf:
- $ref: "#/components/schemas/UserDto"
role:
type: string 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: type:
$ref: "#/components/schemas/NotificationType" $ref: "#/components/schemas/NotificationType"
organizationId: organizationId:
type: string type: string
description: The organization ID for this notification
PagedNotificationDto: PagedNotificationDto:
type: object type: object

View File

@@ -5,20 +5,22 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedExc
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException
import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException 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.ApplicationFormDto
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto 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.Page
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.UUID import java.util.UUID
// import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService
@Service @Service
class ApplicationFormService( class ApplicationFormService(
private val applicationFormRepository: ApplicationFormRepository, private val applicationFormRepository: ApplicationFormRepository,
private val applicationFormMapper: ApplicationFormMapper, private val applicationFormMapper: ApplicationFormMapper,
// private val notificationService: NotificationService private val notificationService: NotificationService,
) { ) {
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto) val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto)
@@ -85,32 +87,29 @@ class ApplicationFormService(
throw ApplicationFormNotUpdatedException(e, id) throw ApplicationFormNotUpdatedException(e, id)
} }
// Create notifications for relevant users createNotificationForOrganization(savedApplicationForm)
createSubmissionNotifications(savedApplicationForm)
return savedApplicationForm return savedApplicationForm
} }
private fun createSubmissionNotifications(applicationForm: ApplicationForm) { private fun createNotificationForOrganization(applicationForm: ApplicationForm) {
val title = "Neuer Mitbestimmungsantrag eingereicht" val title = "Neuer Mitbestimmungsantrag eingereicht"
val message = val message =
"Ein neuer Mitbestimmungsantrag '${applicationForm.name}' wurde von " + "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" val clickTarget = "/application-forms/${applicationForm.id}/0"
// // Create separate notification for each role that should be notified val createNotificationDto =
// val rolesToNotify = listOf("admin", "works_council_member", "employer", "employee") CreateNotificationDto(
// title = title,
// rolesToNotify.forEach { role -> message = message,
// notificationService.createNotification( clickTarget = clickTarget,
// title = title, recipientId = null,
// message = message, targetRoles = null,
// clickTarget = clickTarget, type = NotificationType.INFO,
// recipient = null, organizationId = applicationForm.organizationId,
// role = role, )
// organizationId = applicationForm.organizationId,
// type = NotificationType.INFO notificationService.createNotificationForOrganization(createNotificationDto)
// )
// }
} }
} }

View File

@@ -3,6 +3,7 @@ package com.betriebsratkanzlei.legalconsenthub.config
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtAuthenticationConverter import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtAuthenticationConverter
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration 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.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@@ -14,16 +15,32 @@ import org.springframework.security.web.SecurityFilterChain
@EnableMethodSecurity @EnableMethodSecurity
class SecurityConfig { class SecurityConfig {
@Bean @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, http: HttpSecurity,
customJwtAuthenticationConverter: CustomJwtAuthenticationConverter, customJwtAuthenticationConverter: CustomJwtAuthenticationConverter,
): SecurityFilterChain { ): SecurityFilterChain {
http { http {
csrf { disable() } csrf { disable() }
authorizeHttpRequests { authorizeHttpRequests {
authorize("/swagger-ui/**", permitAll)
authorize("/v3/**", permitAll)
authorize("/actuator/**", permitAll)
authorize(anyRequest, authenticated) authorize(anyRequest, authenticated)
} }
oauth2ResourceServer { oauth2ResourceServer {

View File

@@ -32,9 +32,9 @@ class Notification(
var isRead: Boolean = false, var isRead: Boolean = false,
@ManyToOne @ManyToOne
@JoinColumn(name = "recipient_id", nullable = true) @JoinColumn(name = "recipient_id", nullable = true)
var recipient: User?, var recipient: User? = null,
@Column(nullable = false) @Column(nullable = true, columnDefinition = "TEXT")
var role: String = "", var targetRoles: String? = null,
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
var type: NotificationType = NotificationType.INFO, var type: NotificationType = NotificationType.INFO,

View File

@@ -1,83 +1,152 @@
// package com.betriebsratkanzlei.legalconsenthub.notification package com.betriebsratkanzlei.legalconsenthub.notification
//
// import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
// import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi
// import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
// import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto
// import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto
// import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
// import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.access.prepost.PreAuthorize
// import org.springframework.web.bind.annotation.RestController import org.springframework.security.core.context.SecurityContextHolder
// import java.util.UUID import org.springframework.web.bind.annotation.RestController
// import java.util.UUID
// @RestController
// class NotificationController( @RestController
// private val notificationService: NotificationService, class NotificationController(
// private val notificationMapper: NotificationMapper, private val notificationService: NotificationService,
// private val pagedNotificationMapper: PagedNotificationMapper private val notificationMapper: NotificationMapper,
// ) : NotificationApi { private val pagedNotificationMapper: PagedNotificationMapper,
// ) : NotificationApi {
// override fun getNotifications(page: Int, size: Int): ResponseEntity<PagedNotificationDto> { @PreAuthorize(
// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')",
// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") )
// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") override fun getNotifications(
// organizationId: String,
// val notifications = notificationService.getNotifications( page: Int,
// recipientId = recipientId, size: Int,
// organizationId = organizationId, ): ResponseEntity<PagedNotificationDto> {
// page = page, val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
// size = size val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
// ) validateOrganizationAccess(principal, organizationId)
// val userRoles = principal.roles
// return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications))
// } val notifications =
// notificationService.getNotifications(
// override fun getUnreadNotifications(): ResponseEntity<List<NotificationDto>> { recipientKeycloakId = recipientId,
// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal organizationId = organizationId,
// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") userRoles = userRoles,
// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") page = page,
// size = size,
// val notifications = notificationService.getUnreadNotifications( )
// recipientId = recipientId,
// organizationId = organizationId return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications))
// ) }
//
// return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) }) @PreAuthorize(
// } "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')",
// )
// override fun getUnreadNotificationCount(): ResponseEntity<kotlin.Long> { override fun getUnreadNotifications(organizationId: String): ResponseEntity<List<NotificationDto>> {
// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") validateOrganizationAccess(principal, organizationId)
// val userRoles = principal.roles
// val count = notificationService.getUnreadNotificationCount(
// recipientId = recipientId, val notifications =
// organizationId = organizationId notificationService.getUnreadNotifications(
// ) recipientKeycloakId = recipientId,
// organizationId = organizationId,
// return ResponseEntity.ok(count) userRoles = userRoles,
// } )
//
// override fun markAllNotificationsAsRead(): ResponseEntity<Unit> { return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) })
// 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") override fun getUnreadNotificationCount(
// userId: String,
// notificationService.markAllAsRead( organizationId: String,
// recipientId = recipientId, ): ResponseEntity<kotlin.Long> {
// organizationId = organizationId val count =
// ) notificationService.getUnreadNotificationCount(
// recipientKeycloakId = userId,
// return ResponseEntity.noContent().build() organizationId = organizationId,
// } userRoles = emptyList(),
// )
// override fun markNotificationAsRead(id: UUID): ResponseEntity<NotificationDto> {
// val notification = notificationService.markNotificationAsRead(id) return ResponseEntity.ok(count)
// return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) }
// }
// @PreAuthorize(
// override fun createNotification(createNotificationDto: CreateNotificationDto): ResponseEntity<NotificationDto> { "hasAnyRole('CHIEF_EXECUTIVE_OFFICER', 'BUSINESS_DEPARTMENT', 'IT_DEPARTMENT', 'HUMAN_RESOURCES', 'HEAD_OF_WORKS_COUNCIL', 'WORKS_COUNCIL', 'EMPLOYEE')",
// val notification = notificationService.createNotification(createNotificationDto) )
// return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) override fun markAllNotificationsAsRead(organizationId: String): ResponseEntity<Unit> {
// } 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<NotificationDto> {
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<NotificationDto> {
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<Unit> {
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")
}
}
}

View File

@@ -1,5 +1,6 @@
package com.betriebsratkanzlei.legalconsenthub.notification package com.betriebsratkanzlei.legalconsenthub.notification
import com.betriebsratkanzlei.legalconsenthub.user.User
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto
@@ -17,21 +18,29 @@ class NotificationMapper(
clickTarget = notification.clickTarget, clickTarget = notification.clickTarget,
isRead = notification.isRead, isRead = notification.isRead,
recipient = notification.recipient?.let { userMapper.toUserDto(it) }, recipient = notification.recipient?.let { userMapper.toUserDto(it) },
role = notification.role,
type = notification.type, type = notification.type,
organizationId = notification.organizationId, organizationId = notification.organizationId,
createdAt = notification.createdAt!!, createdAt = notification.createdAt!!,
targetRoles = notification.targetRoles,
) )
fun toNotification(createNotificationDto: CreateNotificationDto): Notification = fun toNotification(
Notification( 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, title = createNotificationDto.title,
message = createNotificationDto.message, message = createNotificationDto.message,
clickTarget = createNotificationDto.clickTarget, clickTarget = createNotificationDto.clickTarget,
isRead = false, recipient = recipient,
recipient = createNotificationDto.recipient?.let { userMapper.toUser(it) }, targetRoles = targetRoles,
role = createNotificationDto.role,
type = createNotificationDto.type, type = createNotificationDto.type,
organizationId = createNotificationDto.organizationId, organizationId = createNotificationDto.organizationId,
) )
} }
}

View File

@@ -1,84 +1,80 @@
// package com.betriebsratkanzlei.legalconsenthub.notification package com.betriebsratkanzlei.legalconsenthub.notification
//
// import org.springframework.data.domain.Page import org.springframework.data.domain.Page
// import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
// import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
// import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Modifying
// import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
// import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
// import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
// import java.util.UUID import java.util.UUID
//
// @Repository @Repository
// interface NotificationRepository : JpaRepository<Notification, UUID> { interface NotificationRepository : JpaRepository<Notification, UUID> {
// @Query(
// fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page<Notification> """
// fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List<Notification> SELECT n FROM Notification n
// fun countByRecipientIdAndIsReadFalse(recipientId: String?): Long WHERE n.organizationId = :organizationId
// AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL)
// @Query( ORDER BY n.createdAt DESC
// """ """,
// SELECT n FROM Notification n WHERE )
// (n.recipient.keycloakId = :recipientId) OR fun findByRecipientOrWithoutRecipientAndOrganizationId(
// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR @Param("keycloakId") keycloakId: String,
// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) @Param("organizationId") organizationId: String,
// ORDER BY n.createdAt DESC pageable: Pageable,
// """ ): Page<Notification>
// )
// fun findUserNotificationsByOrgRole( @Query(
// @Param("recipientId") recipientId: String, """
// @Param("organizationIds") organizationIds: List<String>, SELECT n FROM Notification n
// @Param("orgRolePairs") orgRolePairs: List<String>, WHERE n.organizationId = :organizationId
// pageable: Pageable AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL)
// ): Page<Notification> AND n.isRead = false
// ORDER BY n.createdAt DESC
// @Query( """,
// """ )
// SELECT n FROM Notification n WHERE fun findUnreadByRecipientOrWithoutRecipientAndOrganizationId(
// ((n.recipient.keycloakId = :recipientId) OR @Param("keycloakId") keycloakId: String,
// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR @Param("organizationId") organizationId: String,
// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) ): List<Notification>
// AND n.isRead = false
// ORDER BY n.createdAt DESC @Query(
// """ """
// ) SELECT COUNT(n) FROM Notification n
// fun findUnreadUserNotificationsByOrgRole( WHERE n.organizationId = :organizationId
// @Param("recipientId") recipientId: String, AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL)
// @Param("organizationIds") organizationIds: List<String>, AND n.isRead = false
// @Param("orgRolePairs") orgRolePairs: List<String> """,
// ): List<Notification> )
// fun countUnreadByRecipientOrWithoutRecipientAndOrganizationId(
// @Query( @Param("keycloakId") keycloakId: String,
// """ @Param("organizationId") organizationId: String,
// SELECT COUNT(n) FROM Notification n WHERE ): Long
// ((n.recipient.keycloakId = :recipientId) OR
// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR @Modifying
// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) @Query(
// AND n.isRead = false """
// """ UPDATE Notification n SET n.isRead = true
// ) WHERE n.organizationId = :organizationId
// fun countUnreadUserNotifications( AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL)
// @Param("recipientId") recipientId: String, """,
// @Param("organizationIds") organizationIds: List<String>, )
// @Param("orgRolePairs") orgRolePairs: List<String> fun markAllAsReadByRecipientAndOrganization(
// ): Long @Param("keycloakId") keycloakId: String,
// @Param("organizationId") organizationId: String,
// @Modifying )
// @Query(
// """ @Modifying
// UPDATE Notification n SET n.isRead = true WHERE @Query(
// (n.recipient.keycloakId = :recipientId) OR """
// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR DELETE FROM Notification n
// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) WHERE n.organizationId = :organizationId
// """ AND (n.recipient.keycloakId = :keycloakId OR n.recipient IS NULL)
// ) """,
// fun markAllUserNotificationsAsRead( )
// @Param("recipientId") recipientId: String, fun deleteAllByRecipientAndOrganization(
// @Param("organizationIds") organizationIds: List<String>, @Param("keycloakId") keycloakId: String,
// @Param("orgRolePairs") orgRolePairs: List<String> @Param("organizationId") organizationId: String,
// ) )
// }
// @Modifying
// @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.keycloakId = :recipientId")
// fun markAllAsReadByRecipientId(@Param("recipientId") recipientId: String)
// }

View File

@@ -1,127 +1,163 @@
// package com.betriebsratkanzlei.legalconsenthub.notification package com.betriebsratkanzlei.legalconsenthub.notification
//
// import com.betriebsratkanzlei.legalconsenthub.user.User import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
// import com.betriebsratkanzlei.legalconsenthub.user.UserRepository 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.CreateNotificationDto import org.springframework.data.domain.Page
// import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType import org.springframework.data.domain.PageImpl
// import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest
// import org.springframework.data.domain.PageRequest import org.springframework.security.core.context.SecurityContextHolder
// import org.springframework.stereotype.Service import org.springframework.stereotype.Service
// import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
// import java.util.UUID import java.util.UUID
//
// @Service @Service
// class NotificationService( class NotificationService(
// private val notificationRepository: NotificationRepository, private val notificationRepository: NotificationRepository,
// private val notificationMapper: NotificationMapper, private val userRepository: UserRepository,
// private val userRepository: UserRepository, private val notificationMapper: NotificationMapper,
// private val userRoleConverter: UserRoleConverter ) {
// ) { fun createNotificationForUser(createNotificationDto: CreateNotificationDto): Notification {
// val recipientKeycloakId =
// fun createNotification(createNotificationDto: CreateNotificationDto): Notification { createNotificationDto.recipientId
// val notification = notificationMapper.toNotification(createNotificationDto) ?: throw IllegalArgumentException("recipientId must be provided for user notifications")
// return notificationRepository.save(notification)
// } val recipient =
// userRepository
// fun createNotification( .findById(recipientKeycloakId)
// title: String, .orElseThrow { IllegalArgumentException("User not found with id: $recipientKeycloakId") }
// message: String,
// clickTarget: String, val notification =
// recipient: User?, notificationMapper.toNotification(
// role: String, createNotificationDto = createNotificationDto,
// organizationId: String, recipient = recipient,
// type: NotificationType = NotificationType.INFO )
// ): Notification { return notificationRepository.save(notification)
// val notification = Notification( }
// title = title,
// message = message, @Transactional
// clickTarget = clickTarget, fun createNotificationForRoles(createNotificationDto: CreateNotificationDto): Notification {
// recipient = recipient, val targetRoles =
// role = role, createNotificationDto.targetRoles
// type = type, ?: throw IllegalArgumentException("targetRoles must be provided for role-based notifications")
// organizationId = organizationId
// ) val notification =
// return notificationRepository.save(notification) notificationMapper.toNotification(
// } createNotificationDto = createNotificationDto,
// targetRoles = targetRoles.joinToString(","),
// fun getNotifications( )
// recipientId: String,
// organizationId: String, return notificationRepository.save(notification)
// page: Int = 0, }
// size: Int = 20
// ): Page<Notification> { @Transactional
// val user = userRepository.findById(recipientId) fun createNotificationForOrganization(createNotificationDto: CreateNotificationDto): Notification {
// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } val notification =
// notificationMapper.toNotification(
// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) createNotificationDto = createNotificationDto,
// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } )
//
// val pageable = PageRequest.of(page, size) return notificationRepository.save(notification)
// return if (userRoles.isNotEmpty()) { }
// notificationRepository.findUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs, pageable)
// } else { fun getNotifications(
// notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, pageable) recipientKeycloakId: String,
// } organizationId: String,
// } userRoles: List<String>,
// page: Int = 0,
// fun getUnreadNotifications( size: Int = 20,
// recipientId: String, ): Page<Notification> {
// organizationId: String val pageable = PageRequest.of(page, size)
// ): List<Notification> { val allNotifications =
// val user = userRepository.findById(recipientId) notificationRepository.findByRecipientOrWithoutRecipientAndOrganizationId(
// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } recipientKeycloakId,
// organizationId,
// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) pageable,
// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } )
//
// return if (userRoles.isNotEmpty()) { val filteredContent =
// notificationRepository.findUnreadUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs) allNotifications.content.filter { notification ->
// } else { when {
// notificationRepository.findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId) // Direct recipient notification
// } notification.recipient != null -> true
// } // Organization-wide notification (no recipient, no roles)
// notification.targetRoles.isNullOrEmpty() -> true
// fun getUnreadNotificationCount( // Role-based notification
// recipientId: String, else -> {
// organizationId: String val targetRolesList = notification.targetRoles!!.split(",")
// ): Long { userRoles.any { it in targetRolesList }
// 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 PageImpl(filteredContent, pageable, allNotifications.totalElements)
// }
// return if (userRoles.isNotEmpty()) {
// notificationRepository.countUnreadUserNotifications(recipientId, listOf(organizationId), orgRolePairs) fun getUnreadNotifications(
// } else { recipientKeycloakId: String,
// notificationRepository.countByRecipientIdAndIsReadFalse(recipientId) organizationId: String,
// } userRoles: List<String>,
// } ): List<Notification> {
// val allNotifications =
// @Transactional notificationRepository.findUnreadByRecipientOrWithoutRecipientAndOrganizationId(
// fun markAllAsRead( recipientKeycloakId,
// recipientId: String, organizationId,
// organizationId: String )
// ) {
// val user = userRepository.findById(recipientId) return allNotifications.filter { notification ->
// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } when {
// // Direct recipient notification
// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) notification.recipient != null -> true
// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } // Organization-wide notification (no recipient, no roles)
// notification.targetRoles.isNullOrEmpty() -> true
// if (userRoles.isNotEmpty()) { // Role-based notification
// notificationRepository.markAllUserNotificationsAsRead(recipientId, listOf(organizationId), orgRolePairs) else -> {
// } else { val targetRolesList = notification.targetRoles!!.split(",")
// notificationRepository.markAllAsReadByRecipientId(recipientId) userRoles.any { it in targetRolesList }
// } }
// } }
// }
// @Transactional }
// fun markNotificationAsRead(notificationId: UUID): Notification {
// val notification = notificationRepository.findById(notificationId) fun getUnreadNotificationCount(
// .orElseThrow { IllegalArgumentException("Notification not found with id: $notificationId") } recipientKeycloakId: String,
// notification.isRead = true organizationId: String,
// return notificationRepository.save(notification) userRoles: List<String>,
// } ): 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)
}
}

View File

@@ -17,13 +17,23 @@ class CustomJwtAuthenticationConverter : Converter<Jwt, AbstractAuthenticationTo
val legalconsenthubResource = resourceAccess?.get("legalconsenthub") as? Map<*, *> val legalconsenthubResource = resourceAccess?.get("legalconsenthub") as? Map<*, *>
val roles = (legalconsenthubResource?.get("roles") as? List<*>)?.mapNotNull { it as? String } ?: emptyList() val roles = (legalconsenthubResource?.get("roles") as? List<*>)?.mapNotNull { it as? String } ?: emptyList()
val organizationIds = extractOrganizationIds(jwt)
val authorities: Collection<GrantedAuthority> = val authorities: Collection<GrantedAuthority> =
roles.map { role -> roles.map { role ->
SimpleGrantedAuthority("ROLE_$role") SimpleGrantedAuthority("ROLE_$role")
} }
val principal = CustomJwtTokenPrincipal(userId, username, roles) val principal = CustomJwtTokenPrincipal(userId, username, roles, organizationIds)
return CustomJwtAuthentication(jwt, principal, authorities) return CustomJwtAuthentication(jwt, principal, authorities)
} }
private fun extractOrganizationIds(jwt: Jwt): List<String> {
val organizationClaim = jwt.getClaimAsMap("organization") ?: return emptyList()
return organizationClaim.values.mapNotNull { meta ->
(meta as? Map<*, *>)?.get("id") as? String
}
}
} }

View File

@@ -4,5 +4,5 @@ data class CustomJwtTokenPrincipal(
val id: String? = null, val id: String? = null,
val name: String? = null, val name: String? = null,
val roles: List<String> = emptyList(), val roles: List<String> = emptyList(),
val organizationId: String? = null, val organizationIds: List<String> = emptyList(),
) )

View File

@@ -18,8 +18,8 @@ const color = computed(() => (colorMode.value === 'dark' ? '#111827' : 'white'))
useHead({ useHead({
meta: [ meta: [
{ charset: 'utf-8' }, { charset: 'utf-8' },
{ userName: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ key: 'theme-color', userName: 'theme-color', content: color } { key: 'theme-color', name: 'theme-color', content: color }
], ],
link: [{ rel: 'icon', href: '/favicon.ico' }], link: [{ rel: 'icon', href: '/favicon.ico' }],
htmlAttrs: { htmlAttrs: {
@@ -39,8 +39,4 @@ useSeoMeta({
twitterImage: 'https://dashboard-template.nuxt.dev/social-card.png', twitterImage: 'https://dashboard-template.nuxt.dev/social-card.png',
twitterCard: 'summary_large_image' twitterCard: 'summary_large_image'
}) })
// onBeforeMount(() => {
// $fetch('/api/auth/refresh')
// })
</script> </script>

View File

@@ -53,9 +53,6 @@
> >
{{ notification.type }} {{ notification.type }}
</UBadge> </UBadge>
<UBadge color="neutral" variant="subtle" size="xs">
{{ notification.role }}
</UBadge>
</div> </div>
</div> </div>
</NuxtLink> </NuxtLink>
@@ -80,13 +77,13 @@ const isOpen = computed({
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:modelValue', value)
}) })
// const { notifications, fetchNotifications, handleNotificationClick } = useNotification() const { notifications, fetchNotifications, handleNotificationClick } = useNotification()
//
// watch(isOpen, async (newValue) => { watch(isOpen, async (newValue) => {
// if (newValue) { if (newValue) {
// await fetchNotifications() await fetchNotifications()
// } }
// }) })
function onNotificationClick(notification: NotificationDto) { function onNotificationClick(notification: NotificationDto) {
handleNotificationClick(notification) handleNotificationClick(notification)

View File

@@ -9,15 +9,24 @@ export const useNotification = () => {
markNotificationAsRead markNotificationAsRead
} = useNotificationApi() } = useNotificationApi()
const userStore = useUserStore()
const organizationId = computed(() => userStore.selectedOrganization?.id)
const { user } = useUserSession()
const userId = computed(() => user.value?.keycloakId)
const notifications = ref<NotificationDto[]>([]) const notifications = ref<NotificationDto[]>([])
const unreadNotifications = ref<NotificationDto[]>([]) const unreadNotifications = ref<NotificationDto[]>([])
const unreadCount = ref<number>(0) const unreadCount = ref<number>(0)
const isLoading = ref(false) const isLoading = ref(false)
const fetchNotifications = async (page: number = 0, size: number = 20) => { const fetchNotifications = async (page: number = 0, size: number = 20) => {
if (!organizationId.value) {
console.warn('No organization selected')
return
}
isLoading.value = true isLoading.value = true
try { try {
const response = await getNotifications(page, size) const response = await getNotifications(organizationId.value, page, size)
notifications.value = response.content || [] notifications.value = response.content || []
return response return response
} catch (error) { } catch (error) {
@@ -29,8 +38,12 @@ export const useNotification = () => {
} }
const fetchUnreadNotifications = async () => { const fetchUnreadNotifications = async () => {
if (!organizationId.value) {
console.warn('No organization selected')
return
}
try { try {
const response = await getUnreadNotifications() const response = await getUnreadNotifications(organizationId.value)
unreadNotifications.value = response || [] unreadNotifications.value = response || []
return response return response
} catch (error) { } catch (error) {
@@ -40,8 +53,12 @@ export const useNotification = () => {
} }
const fetchUnreadCount = async () => { const fetchUnreadCount = async () => {
if (!userId.value || !organizationId.value) {
console.warn('No user or organization selected')
return
}
try { try {
const count = await getUnreadNotificationCount() const count = await getUnreadNotificationCount(userId.value, organizationId.value)
unreadCount.value = count || 0 unreadCount.value = count || 0
return count return count
} catch (error) { } catch (error) {
@@ -51,8 +68,12 @@ export const useNotification = () => {
} }
const markAllAsRead = async () => { const markAllAsRead = async () => {
if (!organizationId.value) {
console.warn('No organization selected')
return
}
try { try {
await markAllNotificationsAsRead() await markAllNotificationsAsRead(organizationId.value)
unreadCount.value = 0 unreadCount.value = 0
unreadNotifications.value = [] unreadNotifications.value = []
notifications.value = notifications.value.map((n) => ({ ...n, isRead: true })) notifications.value = notifications.value.map((n) => ({ ...n, isRead: true }))
@@ -63,8 +84,12 @@ export const useNotification = () => {
} }
const markAsRead = async (notificationId: string) => { const markAsRead = async (notificationId: string) => {
if (!organizationId.value) {
console.warn('No organization selected')
return
}
try { try {
await markNotificationAsRead(notificationId) await markNotificationAsRead(notificationId, organizationId.value)
const index = notifications.value.findIndex((n) => n.id === notificationId) const index = notifications.value.findIndex((n) => n.id === notificationId)
if (index !== -1) { if (index !== -1) {
notifications.value[index].isRead = true notifications.value[index].isRead = true

View File

@@ -28,24 +28,28 @@ export function useNotificationApi() {
return notificationApiClient.createNotification({ createNotificationDto }) return notificationApiClient.createNotification({ createNotificationDto })
} }
async function getNotifications(page?: number, size?: number): Promise<PagedNotificationDto> { async function getNotifications(organizationId: string, page?: number, size?: number): Promise<PagedNotificationDto> {
return notificationApiClient.getNotifications({ page, size }) return notificationApiClient.getNotifications({ organizationId, page, size })
} }
async function getUnreadNotifications(): Promise<NotificationDto[]> { async function getUnreadNotifications(organizationId: string): Promise<NotificationDto[]> {
return notificationApiClient.getUnreadNotifications() return notificationApiClient.getUnreadNotifications({ organizationId })
} }
async function getUnreadNotificationCount(): Promise<number> { async function getUnreadNotificationCount(userId: string, organizationId: string): Promise<number> {
return notificationApiClient.getUnreadNotificationCount() return notificationApiClient.getUnreadNotificationCount({ userId, organizationId })
} }
async function markAllNotificationsAsRead(): Promise<void> { async function markAllNotificationsAsRead(organizationId: string): Promise<void> {
return notificationApiClient.markAllNotificationsAsRead() return notificationApiClient.markAllNotificationsAsRead({ organizationId })
} }
async function markNotificationAsRead(id: string): Promise<NotificationDto> { async function markNotificationAsRead(id: string, organizationId: string): Promise<NotificationDto> {
return notificationApiClient.markNotificationAsRead({ id }) return notificationApiClient.markNotificationAsRead({ id, organizationId })
}
async function clearAllNotifications(organizationId: string): Promise<void> {
return notificationApiClient.clearAllNotifications({ organizationId })
} }
return { return {
@@ -54,6 +58,7 @@ export function useNotificationApi() {
getUnreadNotifications, getUnreadNotifications,
getUnreadNotificationCount, getUnreadNotificationCount,
markAllNotificationsAsRead, markAllNotificationsAsRead,
markNotificationAsRead markNotificationAsRead,
clearAllNotifications
} }
} }

View File

@@ -42,10 +42,10 @@ const open = ref(false)
const isNotificationsSlideoverOpen = ref(false) const isNotificationsSlideoverOpen = ref(false)
const { unreadCount, fetchUnreadCount, startPeriodicRefresh } = useNotification() const { unreadCount, fetchUnreadCount, startPeriodicRefresh } = useNotification()
// onMounted(async () => { onMounted(async () => {
// await fetchUnreadCount() await fetchUnreadCount()
// startPeriodicRefresh() startPeriodicRefresh()
// }) })
provide('notificationState', { provide('notificationState', {
isNotificationsSlideoverOpen, isNotificationsSlideoverOpen,

View File

@@ -12,9 +12,6 @@ export default defineEventHandler(async (event: H3Event) => {
const session = await getUserSession(event) const session = await getUserSession(event)
const accessToken = session?.jwt?.accessToken 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) { if (!accessToken) {
throw createError({ throw createError({
statusCode: 401, 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, { return proxyRequest(event, target, {
headers: { headers: {