feat(fullstack): Set user roles per orga, scope notification to orga and role, add orga and role to JWT
This commit is contained in:
@@ -394,6 +394,35 @@ paths:
|
|||||||
$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"
|
||||||
"503":
|
"503":
|
||||||
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
||||||
|
put:
|
||||||
|
summary: Update a user
|
||||||
|
operationId: updateUser
|
||||||
|
description: Updates a user. If no request body is provided, the user data will be synchronized from JWT token claims.
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UserDto"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: User successfully updated
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UserDto"
|
||||||
|
"400":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest"
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"404":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
"503":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable"
|
||||||
delete:
|
delete:
|
||||||
summary: Delete a user
|
summary: Delete a user
|
||||||
operationId: deleteUser
|
operationId: deleteUser
|
||||||
@@ -1049,6 +1078,7 @@ components:
|
|||||||
- id
|
- id
|
||||||
- name
|
- name
|
||||||
- status
|
- status
|
||||||
|
- organizationRoles
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
@@ -1056,8 +1086,13 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
$ref: "#/components/schemas/UserStatus"
|
$ref: "#/components/schemas/UserStatus"
|
||||||
role:
|
organizationRoles:
|
||||||
$ref: "#/components/schemas/UserRole"
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/UserRole"
|
||||||
|
description: "Map of organization IDs to arrays of user roles in those organizations"
|
||||||
|
|
||||||
CreateUserDto:
|
CreateUserDto:
|
||||||
type: object
|
type: object
|
||||||
@@ -1065,7 +1100,6 @@ components:
|
|||||||
- id
|
- id
|
||||||
- name
|
- name
|
||||||
- status
|
- status
|
||||||
- role
|
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
@@ -1073,8 +1107,13 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
$ref: "#/components/schemas/UserStatus"
|
$ref: "#/components/schemas/UserStatus"
|
||||||
role:
|
organizationRoles:
|
||||||
$ref: "#/components/schemas/UserRole"
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/UserRole"
|
||||||
|
description: "Map of organization IDs to arrays of user roles in those organizations"
|
||||||
|
|
||||||
UserStatus:
|
UserStatus:
|
||||||
type: string
|
type: string
|
||||||
@@ -1176,9 +1215,10 @@ components:
|
|||||||
- message
|
- message
|
||||||
- clickTarget
|
- clickTarget
|
||||||
- isRead
|
- isRead
|
||||||
- targetGroup
|
- role
|
||||||
- type
|
- type
|
||||||
- createdAt
|
- createdAt
|
||||||
|
- organizationId
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
@@ -1195,10 +1235,12 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/components/schemas/UserDto"
|
- $ref: "#/components/schemas/UserDto"
|
||||||
targetGroup:
|
role:
|
||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
$ref: "#/components/schemas/NotificationType"
|
$ref: "#/components/schemas/NotificationType"
|
||||||
|
organizationId:
|
||||||
|
type: string
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -1209,8 +1251,9 @@ components:
|
|||||||
- title
|
- title
|
||||||
- message
|
- message
|
||||||
- clickTarget
|
- clickTarget
|
||||||
- targetGroup
|
- role
|
||||||
- type
|
- type
|
||||||
|
- organizationId
|
||||||
properties:
|
properties:
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
@@ -1222,10 +1265,12 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/components/schemas/UserDto"
|
- $ref: "#/components/schemas/UserDto"
|
||||||
targetGroup:
|
role:
|
||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
$ref: "#/components/schemas/NotificationType"
|
$ref: "#/components/schemas/NotificationType"
|
||||||
|
organizationId:
|
||||||
|
type: string
|
||||||
|
|
||||||
PagedNotificationDto:
|
PagedNotificationDto:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -96,44 +96,19 @@ class ApplicationFormService(
|
|||||||
val message = "Ein neuer Mitbestimmungsantrag '${applicationForm.name}' wurde von ${applicationForm.createdBy.name} eingereicht und wartet auf Ihre Bearbeitung."
|
val message = "Ein neuer Mitbestimmungsantrag '${applicationForm.name}' wurde von ${applicationForm.createdBy.name} eingereicht und wartet auf Ihre Bearbeitung."
|
||||||
val clickTarget = "/application-forms/${applicationForm.id}/0"
|
val clickTarget = "/application-forms/${applicationForm.id}/0"
|
||||||
|
|
||||||
// Create notification for admin users
|
// Create separate notification for each role that should be notified
|
||||||
notificationService.createNotificationForUser(
|
val rolesToNotify = listOf("admin", "works_council_member", "employer", "employee")
|
||||||
title = title,
|
|
||||||
message = message,
|
|
||||||
clickTarget = clickTarget,
|
|
||||||
recipient = null,
|
|
||||||
targetGroup = "admin",
|
|
||||||
type = NotificationType.INFO
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create notification for works council members
|
rolesToNotify.forEach { role ->
|
||||||
notificationService.createNotificationForUser(
|
notificationService.createNotification(
|
||||||
title = title,
|
title = title,
|
||||||
message = message,
|
message = message,
|
||||||
clickTarget = clickTarget,
|
clickTarget = clickTarget,
|
||||||
recipient = null,
|
recipient = null,
|
||||||
targetGroup = "works_council_member",
|
role = role,
|
||||||
type = NotificationType.INFO
|
organizationId = applicationForm.organizationId,
|
||||||
)
|
type = NotificationType.INFO
|
||||||
|
)
|
||||||
// Create notification for employer
|
}
|
||||||
notificationService.createNotificationForUser(
|
|
||||||
title = title,
|
|
||||||
message = message,
|
|
||||||
clickTarget = clickTarget,
|
|
||||||
recipient = null,
|
|
||||||
targetGroup = "employer",
|
|
||||||
type = NotificationType.INFO
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create notification for employee
|
|
||||||
notificationService.createNotificationForUser(
|
|
||||||
title = title,
|
|
||||||
message = message,
|
|
||||||
clickTarget = clickTarget,
|
|
||||||
recipient = null,
|
|
||||||
targetGroup = "employee",
|
|
||||||
type = NotificationType.INFO
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.config
|
package com.betriebsratkanzlei.legalconsenthub.config
|
||||||
|
|
||||||
|
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.core.annotation.Order
|
||||||
@@ -20,14 +21,12 @@ class SecurityConfig {
|
|||||||
@Order(1)
|
@Order(1)
|
||||||
fun publicApiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
fun publicApiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
http {
|
http {
|
||||||
securityMatcher("/swagger-ui/**", "/v3/**", "/actuator/**", "/users")
|
securityMatcher("/swagger-ui/**", "/v3/**", "/actuator/**")
|
||||||
csrf { disable() }
|
csrf { disable() }
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
authorize("/swagger-ui/**", permitAll)
|
authorize("/swagger-ui/**", permitAll)
|
||||||
authorize("/v3/**", permitAll)
|
authorize("/v3/**", permitAll)
|
||||||
authorize("/actuator/**", permitAll)
|
authorize("/actuator/**", permitAll)
|
||||||
// For user registration
|
|
||||||
authorize(HttpMethod.POST, "/users", permitAll)
|
|
||||||
authorize(anyRequest, denyAll)
|
authorize(anyRequest, denyAll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +42,8 @@ class SecurityConfig {
|
|||||||
http {
|
http {
|
||||||
csrf { disable() }
|
csrf { disable() }
|
||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
|
// Allow user registration without authentication
|
||||||
|
authorize(HttpMethod.POST, "/users", permitAll)
|
||||||
authorize(anyRequest, authenticated)
|
authorize(anyRequest, authenticated)
|
||||||
}
|
}
|
||||||
oauth2ResourceServer {
|
oauth2ResourceServer {
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ class Notification(
|
|||||||
var recipient: User?,
|
var recipient: User?,
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var targetGroup: String = "",
|
var role: String = "",
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var type: NotificationType = NotificationType.INFO,
|
var type: NotificationType = NotificationType.INFO,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var organizationId: String = "",
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var createdAt: LocalDateTime? = null
|
var createdAt: LocalDateTime? = null
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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.user.UserService
|
|
||||||
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
|
||||||
@@ -15,30 +14,20 @@ import java.util.UUID
|
|||||||
class NotificationController(
|
class NotificationController(
|
||||||
private val notificationService: NotificationService,
|
private val notificationService: NotificationService,
|
||||||
private val notificationMapper: NotificationMapper,
|
private val notificationMapper: NotificationMapper,
|
||||||
private val pagedNotificationMapper: PagedNotificationMapper,
|
private val pagedNotificationMapper: PagedNotificationMapper
|
||||||
private val userService: UserService
|
|
||||||
) : NotificationApi {
|
) : NotificationApi {
|
||||||
|
|
||||||
override fun getNotifications(page: Int, size: Int): ResponseEntity<PagedNotificationDto> {
|
override fun getNotifications(page: Int, size: Int): ResponseEntity<PagedNotificationDto> {
|
||||||
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")
|
||||||
|
|
||||||
val user = userService.getUserById(recipientId)
|
val notifications = notificationService.getNotifications(
|
||||||
|
recipientId = recipientId,
|
||||||
val notifications = if (user.role != null) {
|
organizationId = organizationId,
|
||||||
notificationService.getNotificationsForUserAndGroup(
|
page = page,
|
||||||
recipientId = recipientId,
|
size = size
|
||||||
userRole = user.role!!.value,
|
)
|
||||||
page = page,
|
|
||||||
size = size
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
notificationService.getNotificationsForUser(
|
|
||||||
recipientId = recipientId,
|
|
||||||
page = page,
|
|
||||||
size = size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications))
|
return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications))
|
||||||
}
|
}
|
||||||
@@ -46,29 +35,25 @@ class NotificationController(
|
|||||||
override fun getUnreadNotifications(): ResponseEntity<List<NotificationDto>> {
|
override fun getUnreadNotifications(): 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")
|
||||||
|
|
||||||
val user = userService.getUserById(recipientId)
|
val notifications = notificationService.getUnreadNotifications(
|
||||||
|
recipientId = recipientId,
|
||||||
val notifications = if (user.role != null) {
|
organizationId = organizationId
|
||||||
notificationService.getUnreadNotificationsForUserAndGroup(recipientId, user.role!!.value)
|
)
|
||||||
} else {
|
|
||||||
notificationService.getUnreadNotificationsForUser(recipientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) })
|
return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUnreadNotificationCount(): ResponseEntity<Long> {
|
override fun getUnreadNotificationCount(): ResponseEntity<kotlin.Long> {
|
||||||
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")
|
||||||
|
|
||||||
val user = userService.getUserById(recipientId)
|
val count = notificationService.getUnreadNotificationCount(
|
||||||
|
recipientId = recipientId,
|
||||||
val count = if (user.role != null) {
|
organizationId = organizationId
|
||||||
notificationService.getUnreadNotificationCountForUserAndGroup(recipientId, user.role!!.value)
|
)
|
||||||
} else {
|
|
||||||
notificationService.getUnreadNotificationCount(recipientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.ok(count)
|
return ResponseEntity.ok(count)
|
||||||
}
|
}
|
||||||
@@ -76,22 +61,18 @@ class NotificationController(
|
|||||||
override fun markAllNotificationsAsRead(): ResponseEntity<Unit> {
|
override fun markAllNotificationsAsRead(): ResponseEntity<Unit> {
|
||||||
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")
|
||||||
|
|
||||||
val user = userService.getUserById(recipientId)
|
notificationService.markAllAsRead(
|
||||||
|
recipientId = recipientId,
|
||||||
if (user.role != null) {
|
organizationId = organizationId
|
||||||
notificationService.markAllAsReadForUserAndGroup(recipientId, user.role!!.value)
|
)
|
||||||
} else {
|
|
||||||
notificationService.markAllAsRead(recipientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markNotificationAsRead(id: UUID): ResponseEntity<NotificationDto> {
|
override fun markNotificationAsRead(id: UUID): ResponseEntity<NotificationDto> {
|
||||||
val notification = notificationService.markAsRead(id)
|
val notification = notificationService.markNotificationAsRead(id)
|
||||||
?: return ResponseEntity.notFound().build()
|
|
||||||
|
|
||||||
return ResponseEntity.ok(notificationMapper.toNotificationDto(notification))
|
return ResponseEntity.ok(notificationMapper.toNotificationDto(notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ 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) },
|
||||||
targetGroup = notification.targetGroup,
|
role = notification.role,
|
||||||
type = notification.type,
|
type = notification.type,
|
||||||
|
organizationId = notification.organizationId,
|
||||||
createdAt = notification.createdAt!!
|
createdAt = notification.createdAt!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -31,8 +32,9 @@ class NotificationMapper(
|
|||||||
clickTarget = createNotificationDto.clickTarget,
|
clickTarget = createNotificationDto.clickTarget,
|
||||||
isRead = false,
|
isRead = false,
|
||||||
recipient = createNotificationDto.recipient?.let { userMapper.toUser(it) },
|
recipient = createNotificationDto.recipient?.let { userMapper.toUser(it) },
|
||||||
targetGroup = createNotificationDto.targetGroup,
|
role = createNotificationDto.role,
|
||||||
type = createNotificationDto.type
|
type = createNotificationDto.type,
|
||||||
|
organizationId = createNotificationDto.organizationId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,35 +13,64 @@ import java.util.UUID
|
|||||||
interface NotificationRepository : JpaRepository<Notification, UUID> {
|
interface NotificationRepository : JpaRepository<Notification, UUID> {
|
||||||
|
|
||||||
fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page<Notification>
|
fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page<Notification>
|
||||||
|
|
||||||
fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List<Notification>
|
fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List<Notification>
|
||||||
|
fun countByRecipientIdAndIsReadFalse(recipientId: String?): Long
|
||||||
|
|
||||||
fun findByRecipientIsNullOrderByCreatedAtDesc(pageable: Pageable): Page<Notification>
|
@Query("""
|
||||||
|
SELECT n FROM Notification n WHERE
|
||||||
|
(n.recipient.id = :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<String>,
|
||||||
|
@Param("orgRolePairs") orgRolePairs: List<String>,
|
||||||
|
pageable: Pageable
|
||||||
|
): Page<Notification>
|
||||||
|
|
||||||
fun findByRecipientIsNullAndIsReadFalseOrderByCreatedAtDesc(): List<Notification>
|
@Query("""
|
||||||
|
SELECT n FROM Notification n WHERE
|
||||||
|
((n.recipient.id = :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<String>,
|
||||||
|
@Param("orgRolePairs") orgRolePairs: List<String>
|
||||||
|
): List<Notification>
|
||||||
|
|
||||||
@Query("SELECT n FROM Notification n WHERE (n.recipient.id = :recipientId) OR (n.recipient IS NULL AND n.targetGroup = :targetGroup) OR (n.recipient IS NULL AND n.targetGroup = 'all') ORDER BY n.createdAt DESC")
|
@Query("""
|
||||||
fun findByRecipientIdOrTargetGroupOrderByCreatedAtDesc(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String, pageable: Pageable): Page<Notification>
|
SELECT COUNT(n) FROM Notification n WHERE
|
||||||
|
((n.recipient.id = :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<String>,
|
||||||
|
@Param("orgRolePairs") orgRolePairs: List<String>
|
||||||
|
): Long
|
||||||
|
|
||||||
@Query("SELECT n FROM Notification n WHERE ((n.recipient.id = :recipientId) OR (n.recipient IS NULL AND n.targetGroup = :targetGroup) OR (n.recipient IS NULL AND n.targetGroup = 'all')) AND n.isRead = false ORDER BY n.createdAt DESC")
|
@Modifying
|
||||||
fun findByRecipientIdOrTargetGroupAndIsReadFalseOrderByCreatedAtDesc(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String): List<Notification>
|
@Query("""
|
||||||
|
UPDATE Notification n SET n.isRead = true WHERE
|
||||||
@Query("SELECT COUNT(n) FROM Notification n WHERE ((n.recipient.id = :recipientId) OR (n.recipient IS NULL AND n.targetGroup = :targetGroup) OR (n.recipient IS NULL AND n.targetGroup = 'all')) AND n.isRead = false")
|
(n.recipient.id = :recipientId) OR
|
||||||
fun countByRecipientIdOrTargetGroupAndIsReadFalse(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String): Long
|
(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<String>,
|
||||||
|
@Param("orgRolePairs") orgRolePairs: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :recipientId")
|
@Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :recipientId")
|
||||||
fun markAllAsReadByRecipientId(@Param("recipientId") recipientId: String)
|
fun markAllAsReadByRecipientId(@Param("recipientId") recipientId: String)
|
||||||
|
|
||||||
@Modifying
|
|
||||||
@Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient IS NULL")
|
|
||||||
fun markAllAsReadForNullRecipients()
|
|
||||||
|
|
||||||
@Modifying
|
|
||||||
@Query("UPDATE Notification n SET n.isRead = true WHERE (n.recipient.id = :recipientId) OR (n.recipient IS NULL AND n.targetGroup = :targetGroup) OR (n.recipient IS NULL AND n.targetGroup = 'all')")
|
|
||||||
fun markAllAsReadByRecipientIdOrTargetGroup(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String)
|
|
||||||
|
|
||||||
fun countByRecipientIdAndIsReadFalse(recipientId: String?): Long
|
|
||||||
|
|
||||||
fun countByRecipientIsNullAndIsReadFalse(): Long
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.notification
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub.user.User
|
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.CreateNotificationDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
@@ -12,7 +14,9 @@ import java.util.UUID
|
|||||||
@Service
|
@Service
|
||||||
class NotificationService(
|
class NotificationService(
|
||||||
private val notificationRepository: NotificationRepository,
|
private val notificationRepository: NotificationRepository,
|
||||||
private val notificationMapper: NotificationMapper
|
private val notificationMapper: NotificationMapper,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val userRoleConverter: UserRoleConverter
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createNotification(createNotificationDto: CreateNotificationDto): Notification {
|
fun createNotification(createNotificationDto: CreateNotificationDto): Notification {
|
||||||
@@ -20,12 +24,13 @@ class NotificationService(
|
|||||||
return notificationRepository.save(notification)
|
return notificationRepository.save(notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNotificationForUser(
|
fun createNotification(
|
||||||
title: String,
|
title: String,
|
||||||
message: String,
|
message: String,
|
||||||
clickTarget: String,
|
clickTarget: String,
|
||||||
recipient: User?,
|
recipient: User?,
|
||||||
targetGroup: String,
|
role: String,
|
||||||
|
organizationId: String,
|
||||||
type: NotificationType = NotificationType.INFO
|
type: NotificationType = NotificationType.INFO
|
||||||
): Notification {
|
): Notification {
|
||||||
val notification = Notification(
|
val notification = Notification(
|
||||||
@@ -33,55 +38,90 @@ class NotificationService(
|
|||||||
message = message,
|
message = message,
|
||||||
clickTarget = clickTarget,
|
clickTarget = clickTarget,
|
||||||
recipient = recipient,
|
recipient = recipient,
|
||||||
targetGroup = targetGroup,
|
role = role,
|
||||||
type = type
|
type = type,
|
||||||
|
organizationId = organizationId
|
||||||
)
|
)
|
||||||
return notificationRepository.save(notification)
|
return notificationRepository.save(notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNotificationsForUser(recipientId: String, page: Int = 0, size: Int = 20): Page<Notification> {
|
fun getNotifications(
|
||||||
|
recipientId: String,
|
||||||
|
organizationId: String,
|
||||||
|
page: Int = 0,
|
||||||
|
size: Int = 20
|
||||||
|
): Page<Notification> {
|
||||||
|
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)
|
val pageable = PageRequest.of(page, size)
|
||||||
return notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, pageable)
|
return if (userRoles.isNotEmpty()) {
|
||||||
}
|
notificationRepository.findUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs, pageable)
|
||||||
|
|
||||||
fun getNotificationsForUserAndGroup(recipientId: String, userRole: String, page: Int = 0, size: Int = 20): Page<Notification> {
|
|
||||||
val pageable = PageRequest.of(page, size)
|
|
||||||
return notificationRepository.findByRecipientIdOrTargetGroupOrderByCreatedAtDesc(recipientId, userRole, pageable)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUnreadNotificationsForUser(recipientId: String): List<Notification> {
|
|
||||||
return notificationRepository.findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUnreadNotificationsForUserAndGroup(recipientId: String, userRole: String): List<Notification> {
|
|
||||||
return notificationRepository.findByRecipientIdOrTargetGroupAndIsReadFalseOrderByCreatedAtDesc(recipientId, userRole)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUnreadNotificationCount(recipientId: String): Long {
|
|
||||||
return notificationRepository.countByRecipientIdAndIsReadFalse(recipientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUnreadNotificationCountForUserAndGroup(recipientId: String, userRole: String): Long {
|
|
||||||
return notificationRepository.countByRecipientIdOrTargetGroupAndIsReadFalse(recipientId, userRole)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
fun markAllAsRead(recipientId: String) {
|
|
||||||
notificationRepository.markAllAsReadByRecipientId(recipientId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
fun markAllAsReadForUserAndGroup(recipientId: String, userRole: String) {
|
|
||||||
notificationRepository.markAllAsReadByRecipientIdOrTargetGroup(recipientId, userRole)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markAsRead(notificationId: UUID): Notification? {
|
|
||||||
val notification = notificationRepository.findById(notificationId).orElse(null)
|
|
||||||
return if (notification != null) {
|
|
||||||
notification.isRead = true
|
|
||||||
notificationRepository.save(notification)
|
|
||||||
} else {
|
} else {
|
||||||
null
|
notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, pageable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUnreadNotifications(
|
||||||
|
recipientId: String,
|
||||||
|
organizationId: String
|
||||||
|
): List<Notification> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.config
|
package com.betriebsratkanzlei.legalconsenthub.security
|
||||||
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtAuthentication
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
|
||||||
import org.springframework.core.convert.converter.Converter
|
import org.springframework.core.convert.converter.Converter
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken
|
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||||
import org.springframework.security.core.GrantedAuthority
|
import org.springframework.security.core.GrantedAuthority
|
||||||
@@ -15,7 +13,10 @@ class CustomJwtAuthenticationConverter : Converter<Jwt, AbstractAuthenticationTo
|
|||||||
|
|
||||||
val userId = jwt.getClaimAsString("id")
|
val userId = jwt.getClaimAsString("id")
|
||||||
val username = jwt.getClaimAsString("name")
|
val username = jwt.getClaimAsString("name")
|
||||||
val principal = CustomJwtTokenPrincipal(userId, username)
|
val organizationId = jwt.getClaimAsString("organizationId")
|
||||||
|
val roles = jwt.getClaimAsStringList("roles") ?: emptyList()
|
||||||
|
|
||||||
|
val principal = CustomJwtTokenPrincipal(userId, username, organizationId, roles)
|
||||||
|
|
||||||
return CustomJwtAuthentication(jwt, principal, authorities)
|
return CustomJwtAuthentication(jwt, principal, authorities)
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,7 @@ package com.betriebsratkanzlei.legalconsenthub.security
|
|||||||
|
|
||||||
data class CustomJwtTokenPrincipal(
|
data class CustomJwtTokenPrincipal(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val name: String? = null
|
val name: String? = null,
|
||||||
|
val organizationId: String? = null,
|
||||||
|
val roles: List<String> = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.user
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.UserRole
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
import org.springframework.data.annotation.CreatedDate
|
import org.springframework.data.annotation.CreatedDate
|
||||||
@@ -23,9 +22,9 @@ class User(
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var status: UserStatus = UserStatus.ACTIVE,
|
var status: UserStatus = UserStatus.ACTIVE,
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@ElementCollection
|
||||||
@Column(nullable = true)
|
@CollectionTable(name = "user_organization_roles", joinColumns = [JoinColumn(name = "user_id")])
|
||||||
var role: UserRole? = null,
|
var organizationRoles: MutableSet<UserOrganizationRole> = mutableSetOf(),
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.user
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.api.UserApi
|
import com.betriebsratkanzlei.legalconsenthub_api.api.UserApi
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -22,6 +24,22 @@ class UserController(
|
|||||||
return ResponseEntity.ok(userMapper.toUserDto(user))
|
return ResponseEntity.ok(userMapper.toUserDto(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateUser(id: String, userDto: UserDto?): ResponseEntity<UserDto> {
|
||||||
|
val user = if (userDto != null) {
|
||||||
|
// Update with provided data
|
||||||
|
userService.updateUser(id, userDto)
|
||||||
|
} else {
|
||||||
|
// Update from JWT data
|
||||||
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
|
val userId = principal.id ?: throw IllegalArgumentException("User ID missing from JWT")
|
||||||
|
val organizationId = principal.organizationId
|
||||||
|
val roles = principal.roles
|
||||||
|
|
||||||
|
userService.updateUserFromJwt(userId, organizationId, roles)
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(userMapper.toUserDto(user))
|
||||||
|
}
|
||||||
|
|
||||||
override fun deleteUser(id: String): ResponseEntity<Unit> {
|
override fun deleteUser(id: String): ResponseEntity<Unit> {
|
||||||
userService.deleteUser(id)
|
userService.deleteUser(id)
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
|
|||||||
@@ -4,22 +4,31 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
|
|||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class UserMapper() {
|
class UserMapper(
|
||||||
|
private val roleConverter: UserRoleConverter
|
||||||
|
) {
|
||||||
fun toUserDto(user: User): UserDto {
|
fun toUserDto(user: User): UserDto {
|
||||||
|
val organizationRolesDto = roleConverter.convertToMap(user.organizationRoles)
|
||||||
|
|
||||||
return UserDto(
|
return UserDto(
|
||||||
id = user.id,
|
id = user.id,
|
||||||
name = user.name,
|
name = user.name,
|
||||||
status = user.status,
|
status = user.status,
|
||||||
role = user.role
|
organizationRoles = organizationRolesDto
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toUser(userDto: UserDto): User {
|
fun toUser(userDto: UserDto): User {
|
||||||
return User(
|
val user = User(
|
||||||
id = userDto.id,
|
id = userDto.id,
|
||||||
name = userDto.name,
|
name = userDto.name,
|
||||||
status = userDto.status,
|
status = userDto.status
|
||||||
role = userDto.role
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
userDto.organizationRoles.forEach { (orgId, roles) ->
|
||||||
|
roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Embeddable
|
||||||
|
|
||||||
|
@Embeddable
|
||||||
|
data class UserOrganizationRole(
|
||||||
|
@Column(name = "organization_id", nullable = false)
|
||||||
|
val organizationId: String,
|
||||||
|
|
||||||
|
@Column(name = "role", nullable = false)
|
||||||
|
val role: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserRole
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
object UserRoleConverter {
|
||||||
|
|
||||||
|
fun getRolesForOrganization(organizationRoles: Set<UserOrganizationRole>, organizationId: String): List<UserRole> {
|
||||||
|
return organizationRoles
|
||||||
|
.filter { it.organizationId == organizationId }
|
||||||
|
.mapNotNull { orgRole ->
|
||||||
|
try {
|
||||||
|
UserRole.valueOf(orgRole.role)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRolesForOrganization(organizationRoles: MutableSet<UserOrganizationRole>, organizationId: String, roles: List<UserRole>) {
|
||||||
|
organizationRoles.removeIf { it.organizationId == organizationId }
|
||||||
|
roles.forEach { role ->
|
||||||
|
organizationRoles.add(UserOrganizationRole(organizationId, role.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToMap(organizationRoles: Set<UserOrganizationRole>): Map<String, List<UserRole>> {
|
||||||
|
return organizationRoles
|
||||||
|
.groupBy { it.organizationId }
|
||||||
|
.mapValues { (_, roles) ->
|
||||||
|
roles.mapNotNull { orgRole ->
|
||||||
|
try {
|
||||||
|
UserRole.valueOf(orgRole.role)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filterValues { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@ import com.betriebsratkanzlei.legalconsenthub.error.UserAlreadyExistsException
|
|||||||
import com.betriebsratkanzlei.legalconsenthub.error.UserNotFoundException
|
import com.betriebsratkanzlei.legalconsenthub.error.UserNotFoundException
|
||||||
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
|
||||||
|
import jakarta.transaction.Transactional
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class UserService(
|
class UserService(
|
||||||
private val userRepository: UserRepository
|
private val userRepository: UserRepository,
|
||||||
|
private val roleConverter: UserRoleConverter
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun getCurrentUser(): User {
|
fun getCurrentUser(): User {
|
||||||
@@ -29,9 +32,13 @@ class UserService(
|
|||||||
val user = User(
|
val user = User(
|
||||||
id = createUserDto.id,
|
id = createUserDto.id,
|
||||||
name = createUserDto.name,
|
name = createUserDto.name,
|
||||||
status = createUserDto.status ?: UserStatus.ACTIVE,
|
status = createUserDto.status
|
||||||
role = createUserDto.role
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
createUserDto.organizationRoles?.forEach { (orgId, roles) ->
|
||||||
|
roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles)
|
||||||
|
}
|
||||||
|
|
||||||
return userRepository.save(user)
|
return userRepository.save(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +47,44 @@ class UserService(
|
|||||||
.orElseThrow { UserNotFoundException(userId) }
|
.orElseThrow { UserNotFoundException(userId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun updateUser(userId: String, userDto: UserDto): User {
|
||||||
|
val user = userRepository.findById(userId)
|
||||||
|
.orElseThrow { UserNotFoundException(userId) }
|
||||||
|
|
||||||
|
user.name = userDto.name
|
||||||
|
user.status = userDto.status
|
||||||
|
|
||||||
|
user.organizationRoles.clear()
|
||||||
|
userDto.organizationRoles.forEach { (orgId, roles) ->
|
||||||
|
roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRepository.save(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun updateUserFromJwt(userId: String, jwtOrganizationId: String?, jwtRoles: List<String>?): User {
|
||||||
|
val existingUser = userRepository.findById(userId)
|
||||||
|
.orElseThrow { UserNotFoundException(userId) }
|
||||||
|
|
||||||
|
if (jwtOrganizationId != null && !jwtRoles.isNullOrEmpty()) {
|
||||||
|
existingUser.organizationRoles.removeIf { it.organizationId == jwtOrganizationId }
|
||||||
|
|
||||||
|
jwtRoles.forEach { role ->
|
||||||
|
val normalizedRole = role.lowercase().replace("_", "_")
|
||||||
|
existingUser.organizationRoles.add(
|
||||||
|
UserOrganizationRole(
|
||||||
|
organizationId = jwtOrganizationId,
|
||||||
|
role = normalizedRole
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userRepository.save(existingUser)
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteUser(userId: String) {
|
fun deleteUser(userId: String) {
|
||||||
userRepository.deleteById(userId)
|
userRepository.deleteById(userId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ create table app_user
|
|||||||
modified_at timestamp(6) not null,
|
modified_at timestamp(6) not null,
|
||||||
id varchar(255) not null,
|
id varchar(255) not null,
|
||||||
name varchar(255) not null,
|
name varchar(255) not null,
|
||||||
role varchar(255),
|
|
||||||
status varchar(255) not null check (status in ('INVITED', 'ACTIVE', 'BLOCKED', 'SUSPENDED_SUBSCRIPTION')),
|
status varchar(255) not null check (status in ('INVITED', 'ACTIVE', 'BLOCKED', 'SUSPENDED_SUBSCRIPTION')),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
@@ -66,18 +65,27 @@ create table form_element_section
|
|||||||
|
|
||||||
create table notification
|
create table notification
|
||||||
(
|
(
|
||||||
is_read boolean not null,
|
is_read boolean not null,
|
||||||
created_at timestamp(6) not null,
|
created_at timestamp(6) not null,
|
||||||
id uuid not null,
|
id uuid not null,
|
||||||
click_target varchar(255) not null,
|
click_target varchar(255) not null,
|
||||||
message TEXT not null,
|
message TEXT not null,
|
||||||
recipient_id varchar(255),
|
organization_id varchar(255) not null,
|
||||||
target_group varchar(255) not null,
|
recipient_id varchar(255),
|
||||||
title varchar(255) not null,
|
role varchar(255) not null,
|
||||||
type varchar(255) not null check (type in ('INFO', 'WARNING', 'ERROR')),
|
title varchar(255) not null,
|
||||||
|
type varchar(255) not null check (type in ('INFO', 'WARNING', 'ERROR')),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table user_organization_roles
|
||||||
|
(
|
||||||
|
organization_id varchar(255) not null,
|
||||||
|
role varchar(255) not null,
|
||||||
|
user_id varchar(255) not null,
|
||||||
|
primary key (organization_id, role, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
alter table if exists application_form
|
alter table if exists application_form
|
||||||
add constraint FKhtad5onoy2jknhtyfmx6cvvey
|
add constraint FKhtad5onoy2jknhtyfmx6cvvey
|
||||||
foreign key (created_by_id)
|
foreign key (created_by_id)
|
||||||
@@ -122,3 +130,8 @@ alter table if exists notification
|
|||||||
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
|
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
|
||||||
foreign key (recipient_id)
|
foreign key (recipient_id)
|
||||||
references app_user;
|
references app_user;
|
||||||
|
|
||||||
|
alter table if exists user_organization_roles
|
||||||
|
add constraint FKhgmm93qre3up6hy63wcef3yqk
|
||||||
|
foreign key (user_id)
|
||||||
|
references app_user
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function useAuth() {
|
|||||||
redirectGuestTo: '/login'
|
redirectGuestTo: '/login'
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fetchSession() {
|
async function fetchSession(targetPath?: string) {
|
||||||
if (sessionFetching.value) {
|
if (sessionFetching.value) {
|
||||||
console.log('already fetching session')
|
console.log('already fetching session')
|
||||||
return
|
return
|
||||||
@@ -109,7 +109,7 @@ export function useAuth() {
|
|||||||
sessionFetching.value = false
|
sessionFetching.value = false
|
||||||
|
|
||||||
// Only fetch JWT and organizations if we have a session and not on public routes
|
// Only fetch JWT and organizations if we have a session and not on public routes
|
||||||
if (session.value && !isPublicRoute()) {
|
if (session.value && !isPublicPath(targetPath)) {
|
||||||
await fetchJwtAndOrganizations()
|
await fetchJwtAndOrganizations()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +160,10 @@ export function useAuth() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPublicRoute(routeToCheck?: RouteLocationNormalizedLoaded) {
|
function isPublicPath(path?: string) {
|
||||||
const finalRoute = routeToCheck ?? route
|
const finalPath = path ?? route.path
|
||||||
const publicRoutes = ['/login', '/signup', '/accept-invitation']
|
const publicRoutes = ['/login', '/signup', '/accept-invitation']
|
||||||
return publicRoutes.some((path) => finalRoute.path.startsWith(path))
|
return publicRoutes.some((path) => finalPath.startsWith(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signOut({ redirectTo }: { redirectTo?: RouteLocationRaw } = {}) {
|
async function signOut({ redirectTo }: { redirectTo?: RouteLocationRaw } = {}) {
|
||||||
@@ -195,7 +195,7 @@ export function useAuth() {
|
|||||||
fetchSession,
|
fetchSession,
|
||||||
client,
|
client,
|
||||||
jwt,
|
jwt,
|
||||||
isPublicRoute,
|
isPublicPath,
|
||||||
activeMember
|
activeMember
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,19 @@ export function useUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateUser(id: string, userDto?: UserDto): Promise<UserDto> {
|
||||||
|
try {
|
||||||
|
return await userApi.updateUser(id, userDto)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof ResponseError) {
|
||||||
|
console.error(`Failed updating user with ID ${id}:`, e.response)
|
||||||
|
} else {
|
||||||
|
console.error(`Failed updating user with ID ${id}:`, e)
|
||||||
|
}
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteUser(id: string): Promise<void> {
|
async function deleteUser(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return await userApi.deleteUser(id)
|
return await userApi.deleteUser(id)
|
||||||
@@ -53,6 +66,7 @@ export function useUser() {
|
|||||||
return {
|
return {
|
||||||
createUser,
|
createUser,
|
||||||
getUserById,
|
getUserById,
|
||||||
|
updateUser,
|
||||||
deleteUser
|
deleteUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { UserApi, Configuration, type CreateUserDto, type UserDto } from '~/.api-client'
|
||||||
UserApi,
|
|
||||||
Configuration,
|
|
||||||
type CreateUserDto,
|
|
||||||
type UserDto
|
|
||||||
} from '~/.api-client'
|
|
||||||
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
||||||
|
|
||||||
export function useUserApi() {
|
export function useUserApi() {
|
||||||
@@ -15,25 +10,32 @@ export function useUserApi() {
|
|||||||
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
|
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
|
||||||
)
|
)
|
||||||
|
|
||||||
const userApiClient = new UserApi(
|
// Track changing JWT of user who accepts the invitation
|
||||||
new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })
|
const userApiClient = computed(
|
||||||
|
() =>
|
||||||
|
new UserApi(new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }))
|
||||||
)
|
)
|
||||||
|
|
||||||
async function createUser(createUserDto: CreateUserDto): Promise<UserDto> {
|
async function createUser(createUserDto: CreateUserDto): Promise<UserDto> {
|
||||||
return userApiClient.createUser({ createUserDto })
|
return userApiClient.value.createUser({ createUserDto })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserById(id: string): Promise<UserDto> {
|
async function getUserById(id: string): Promise<UserDto> {
|
||||||
return userApiClient.getUserById({ id })
|
return userApiClient.value.getUserById({ id })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUser(id: string, userDto?: UserDto): Promise<UserDto> {
|
||||||
|
return userApiClient.value.updateUser({ id, userDto })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(id: string): Promise<void> {
|
async function deleteUser(id: string): Promise<void> {
|
||||||
return userApiClient.deleteUser({ id })
|
return userApiClient.value.deleteUser({ id })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createUser,
|
createUser,
|
||||||
getUserById,
|
getUserById,
|
||||||
|
updateUser,
|
||||||
deleteUser
|
deleteUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) =>
|
|||||||
console.log('[1] Auth middleware disabled for this route:', to.path)
|
console.log('[1] Auth middleware disabled for this route:', to.path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { loggedIn, options, fetchSession, isPublicRoute } = useAuth()
|
const { loggedIn, options, fetchSession, isPublicPath } = useAuth()
|
||||||
const { only, redirectUserTo, redirectGuestTo } = defu(to.meta?.auth, options)
|
const { only, redirectUserTo, redirectGuestTo } = defu(to.meta?.auth, options)
|
||||||
|
|
||||||
// 2. If guest mode, redirect if authenticated
|
// 2. If guest mode, redirect if authenticated
|
||||||
@@ -55,7 +55,7 @@ export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) =>
|
|||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
console.log('[3] Client-side navigation, fetching session')
|
console.log('[3] Client-side navigation, fetching session')
|
||||||
try {
|
try {
|
||||||
await fetchSession()
|
await fetchSession(to.path)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) =>
|
|||||||
|
|
||||||
// 4. If not authenticated, redirect to home or guest route
|
// 4. If not authenticated, redirect to home or guest route
|
||||||
if (!loggedIn.value) {
|
if (!loggedIn.value) {
|
||||||
if (isPublicRoute(to)) {
|
if (isPublicPath(to.path)) {
|
||||||
console.log('[4] Not authenticated, but route is public:', to.path)
|
console.log('[4] Not authenticated, but route is public:', to.path)
|
||||||
// Continue navigating to the public route
|
// Continue navigating to the public route
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"@nuxtjs/i18n": "10.0.3",
|
"@nuxtjs/i18n": "10.0.3",
|
||||||
"@pinia/nuxt": "0.10.1",
|
"@pinia/nuxt": "0.10.1",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.6.0",
|
||||||
"better-auth": "1.3.4",
|
"better-auth": "1.3.9",
|
||||||
"better-sqlite3": "11.8.1",
|
"better-sqlite3": "11.8.1",
|
||||||
"nuxt": "3.16.1",
|
"nuxt": "3.16.1",
|
||||||
"pinia": "3.0.1",
|
"pinia": "3.0.1",
|
||||||
|
|||||||
125
legalconsenthub/pnpm-lock.yaml
generated
125
legalconsenthub/pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/ui-pro':
|
'@nuxt/ui-pro':
|
||||||
specifier: 3.1.1
|
specifier: 3.1.1
|
||||||
version: 3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.0.10)
|
version: 3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.1.8)
|
||||||
'@nuxtjs/i18n':
|
'@nuxtjs/i18n':
|
||||||
specifier: 10.0.3
|
specifier: 10.0.3
|
||||||
version: 10.0.3(@vue/compiler-dom@3.5.18)(db0@0.3.1(better-sqlite3@11.8.1))(eslint@9.20.1(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(rollup@4.38.0)(vue@3.5.13(typescript@5.7.3))
|
version: 10.0.3(@vue/compiler-dom@3.5.18)(db0@0.3.1(better-sqlite3@11.8.1))(eslint@9.20.1(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(rollup@4.38.0)(vue@3.5.13(typescript@5.7.3))
|
||||||
@@ -21,8 +21,8 @@ importers:
|
|||||||
specifier: ^13.6.0
|
specifier: ^13.6.0
|
||||||
version: 13.6.0(vue@3.5.13(typescript@5.7.3))
|
version: 13.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.3.4
|
specifier: 1.3.9
|
||||||
version: 1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.3.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: 11.8.1
|
specifier: 11.8.1
|
||||||
version: 11.8.1
|
version: 11.8.1
|
||||||
@@ -243,8 +243,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@better-auth/utils@0.2.5':
|
'@better-auth/utils@0.2.6':
|
||||||
resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==}
|
resolution: {integrity: sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA==}
|
||||||
|
|
||||||
'@better-fetch/fetch@1.1.18':
|
'@better-fetch/fetch@1.1.18':
|
||||||
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
|
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
|
||||||
@@ -1031,12 +1031,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-z6okREyK8in0486a22Oro0k+YsuyEjDXJt46FpgeOgXqKJ9ElM8QPll0iuLBkpbH33ENiNbIPLd1cuClRQnhiw==}
|
resolution: {integrity: sha512-z6okREyK8in0486a22Oro0k+YsuyEjDXJt46FpgeOgXqKJ9ElM8QPll0iuLBkpbH33ENiNbIPLd1cuClRQnhiw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@noble/ciphers@0.6.0':
|
'@noble/ciphers@2.0.0':
|
||||||
resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==}
|
resolution: {integrity: sha512-j/l6jpnpaIBM87cAYPJzi/6TgqmBv9spkqPyCXvRYsu5uxqh6tPJZDnD85yo8VWqzTuTQPgfv7NgT63u7kbwAQ==}
|
||||||
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
'@noble/hashes@1.8.0':
|
'@noble/hashes@2.0.0':
|
||||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
resolution: {integrity: sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw==}
|
||||||
engines: {node: ^14.21.3 || >=16}
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
@@ -1694,11 +1695,11 @@ packages:
|
|||||||
'@selderee/plugin-htmlparser2@0.11.0':
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
'@simplewebauthn/browser@13.1.0':
|
'@simplewebauthn/browser@13.1.2':
|
||||||
resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==}
|
resolution: {integrity: sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==}
|
||||||
|
|
||||||
'@simplewebauthn/server@13.1.1':
|
'@simplewebauthn/server@13.1.2':
|
||||||
resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==}
|
resolution: {integrity: sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@sindresorhus/is@7.0.1':
|
'@sindresorhus/is@7.0.1':
|
||||||
@@ -2357,19 +2358,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
|
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
better-auth@1.3.4:
|
better-auth@1.3.9:
|
||||||
resolution: {integrity: sha512-JbZYam6Cs3Eu5CSoMK120zSshfaKvrCftSo/+v7524H1RvhryQ7UtMbzagBcXj0Digjj8hZtVkkR4tTZD/wK2g==}
|
resolution: {integrity: sha512-Ty6BHzuShlqSs7I4RMlBRQ3duOWNB7WWriIu2FJVGjQAOtTVvamzFCR4/j5ROFLoNkpvNTRF7BJozsrMICL1gw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
'@lynx-js/react': '*'
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
'@lynx-js/react':
|
||||||
|
optional: true
|
||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
better-call@1.0.12:
|
better-call@1.0.18:
|
||||||
resolution: {integrity: sha512-ssq5OfB9Ungv2M1WVrRnMBomB0qz1VKuhkY2WxjHaLtlsHoSe9EPolj1xf7xf8LY9o3vfk3Rx6rCWI4oVHeBRg==}
|
resolution: {integrity: sha512-Ojyck3P3fs/egBmCW50tvfbCJorNV5KphfPOKrkCxPfOr8Brth1ruDtAJuhHVHEUiWrXv+vpEgWQk7m7FzhbbQ==}
|
||||||
|
|
||||||
better-sqlite3@11.8.1:
|
better-sqlite3@11.8.1:
|
||||||
resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==}
|
resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==}
|
||||||
@@ -3688,8 +3692,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
jose@5.10.0:
|
jose@6.1.0:
|
||||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
@@ -3766,8 +3770,8 @@ packages:
|
|||||||
kolorist@1.8.0:
|
kolorist@1.8.0:
|
||||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||||
|
|
||||||
kysely@0.28.3:
|
kysely@0.28.6:
|
||||||
resolution: {integrity: sha512-svKnkSH72APRdjfVCCOknxaC9Eb3nA2StHG9d5/sKOqRvHRp2Dtf1XwDvc92b4B5v6LV+EAGWXQbZ5jMOvHaDw==}
|
resolution: {integrity: sha512-QQlpW/Db5yhhY9+c1jiCBCUCJqZoWLw6c1rE+H+FqGujMuIAxerCSdQNvyP3zyhQUO913J9Ank1NsQEb5a15mA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
launch-editor@2.10.0:
|
launch-editor@2.10.0:
|
||||||
@@ -5746,33 +5750,33 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.24.1
|
zod: ^3.24.1
|
||||||
|
|
||||||
zod@4.0.10:
|
zod@4.1.8:
|
||||||
resolution: {integrity: sha512-3vB+UU3/VmLL2lvwcY/4RV2i9z/YU0DTV/tDuYjrwmx5WeJ7hwy+rGEEx8glHp6Yxw7ibRbKSaIFBgReRPe5KA==}
|
resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@2.2.8(zod@4.0.10)':
|
'@ai-sdk/provider-utils@2.2.8(zod@4.1.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 1.1.3
|
||||||
nanoid: 3.3.11
|
nanoid: 3.3.11
|
||||||
secure-json-parse: 2.7.0
|
secure-json-parse: 2.7.0
|
||||||
zod: 4.0.10
|
zod: 4.1.8
|
||||||
|
|
||||||
'@ai-sdk/provider@1.1.3':
|
'@ai-sdk/provider@1.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
json-schema: 0.4.0
|
json-schema: 0.4.0
|
||||||
|
|
||||||
'@ai-sdk/ui-utils@1.2.11(zod@4.0.10)':
|
'@ai-sdk/ui-utils@1.2.11(zod@4.1.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.1.3
|
'@ai-sdk/provider': 1.1.3
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@4.0.10)
|
'@ai-sdk/provider-utils': 2.2.8(zod@4.1.8)
|
||||||
zod: 4.0.10
|
zod: 4.1.8
|
||||||
zod-to-json-schema: 3.24.5(zod@4.0.10)
|
zod-to-json-schema: 3.24.5(zod@4.1.8)
|
||||||
|
|
||||||
'@ai-sdk/vue@1.2.12(vue@3.5.13(typescript@5.7.3))(zod@4.0.10)':
|
'@ai-sdk/vue@1.2.12(vue@3.5.13(typescript@5.7.3))(zod@4.1.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider-utils': 2.2.8(zod@4.0.10)
|
'@ai-sdk/provider-utils': 2.2.8(zod@4.1.8)
|
||||||
'@ai-sdk/ui-utils': 1.2.11(zod@4.0.10)
|
'@ai-sdk/ui-utils': 1.2.11(zod@4.1.8)
|
||||||
swrv: 1.1.0(vue@3.5.13(typescript@5.7.3))
|
swrv: 1.1.0(vue@3.5.13(typescript@5.7.3))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vue: 3.5.13(typescript@5.7.3)
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
@@ -5981,9 +5985,8 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.27.1
|
'@babel/helper-validator-identifier': 7.27.1
|
||||||
|
|
||||||
'@better-auth/utils@0.2.5':
|
'@better-auth/utils@0.2.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.8.3
|
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
|
|
||||||
'@better-fetch/fetch@1.1.18': {}
|
'@better-fetch/fetch@1.1.18': {}
|
||||||
@@ -6613,9 +6616,9 @@ snapshots:
|
|||||||
|
|
||||||
'@netlify/serverless-functions-api@1.36.0': {}
|
'@netlify/serverless-functions-api@1.36.0': {}
|
||||||
|
|
||||||
'@noble/ciphers@0.6.0': {}
|
'@noble/ciphers@2.0.0': {}
|
||||||
|
|
||||||
'@noble/hashes@1.8.0': {}
|
'@noble/hashes@2.0.0': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6983,12 +6986,12 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxt/ui-pro@3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.0.10)':
|
'@nuxt/ui-pro@3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.1.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/vue': 1.2.12(vue@3.5.13(typescript@5.7.3))(zod@4.0.10)
|
'@ai-sdk/vue': 1.2.12(vue@3.5.13(typescript@5.7.3))(zod@4.1.8)
|
||||||
'@nuxt/kit': 3.17.2(magicast@0.3.5)
|
'@nuxt/kit': 3.17.2(magicast@0.3.5)
|
||||||
'@nuxt/schema': 3.17.2
|
'@nuxt/schema': 3.17.2
|
||||||
'@nuxt/ui': 3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.0.10)
|
'@nuxt/ui': 3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.1.8)
|
||||||
'@standard-schema/spec': 1.0.0
|
'@standard-schema/spec': 1.0.0
|
||||||
'@vueuse/core': 13.6.0(vue@3.5.13(typescript@5.7.3))
|
'@vueuse/core': 13.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
@@ -7006,7 +7009,7 @@ snapshots:
|
|||||||
unplugin-auto-import: 19.1.2(@nuxt/kit@3.17.2(magicast@0.3.5))(@vueuse/core@13.6.0(vue@3.5.13(typescript@5.7.3)))
|
unplugin-auto-import: 19.1.2(@nuxt/kit@3.17.2(magicast@0.3.5))(@vueuse/core@13.6.0(vue@3.5.13(typescript@5.7.3)))
|
||||||
unplugin-vue-components: 28.5.0(@babel/parser@7.28.0)(@nuxt/kit@3.17.2(magicast@0.3.5))(vue@3.5.13(typescript@5.7.3))
|
unplugin-vue-components: 28.5.0(@babel/parser@7.28.0)(@nuxt/kit@3.17.2(magicast@0.3.5))(vue@3.5.13(typescript@5.7.3))
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 4.0.10
|
zod: 4.1.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@azure/app-configuration'
|
- '@azure/app-configuration'
|
||||||
- '@azure/cosmos'
|
- '@azure/cosmos'
|
||||||
@@ -7047,7 +7050,7 @@ snapshots:
|
|||||||
- vue
|
- vue
|
||||||
- vue-router
|
- vue-router
|
||||||
|
|
||||||
'@nuxt/ui@3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.0.10)':
|
'@nuxt/ui@3.1.1(@babel/parser@7.28.0)(axios@1.7.9)(db0@0.3.1(better-sqlite3@11.8.1))(embla-carousel@8.6.0)(ioredis@5.6.0)(magicast@0.3.5)(typescript@5.7.3)(vite@6.2.3(@types/node@22.13.14)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))(zod@4.1.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/vue': 4.3.0(vue@3.5.13(typescript@5.7.3))
|
'@iconify/vue': 4.3.0(vue@3.5.13(typescript@5.7.3))
|
||||||
'@internationalized/date': 3.8.0
|
'@internationalized/date': 3.8.0
|
||||||
@@ -7094,7 +7097,7 @@ snapshots:
|
|||||||
vue-component-type-helpers: 2.2.10
|
vue-component-type-helpers: 2.2.10
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vue-router: 4.5.0(vue@3.5.13(typescript@5.7.3))
|
vue-router: 4.5.0(vue@3.5.13(typescript@5.7.3))
|
||||||
zod: 4.0.10
|
zod: 4.1.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@azure/app-configuration'
|
- '@azure/app-configuration'
|
||||||
- '@azure/cosmos'
|
- '@azure/cosmos'
|
||||||
@@ -7652,9 +7655,9 @@ snapshots:
|
|||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
|
|
||||||
'@simplewebauthn/browser@13.1.0': {}
|
'@simplewebauthn/browser@13.1.2': {}
|
||||||
|
|
||||||
'@simplewebauthn/server@13.1.1':
|
'@simplewebauthn/server@13.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hexagon/base64': 1.1.28
|
'@hexagon/base64': 1.1.28
|
||||||
'@levischuck/tiny-cbor': 0.2.11
|
'@levischuck/tiny-cbor': 0.2.11
|
||||||
@@ -8395,25 +8398,25 @@ snapshots:
|
|||||||
|
|
||||||
basic-ftp@5.0.5: {}
|
basic-ftp@5.0.5: {}
|
||||||
|
|
||||||
better-auth@1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
better-auth@1.3.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/utils': 0.2.5
|
'@better-auth/utils': 0.2.6
|
||||||
'@better-fetch/fetch': 1.1.18
|
'@better-fetch/fetch': 1.1.18
|
||||||
'@noble/ciphers': 0.6.0
|
'@noble/ciphers': 2.0.0
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 2.0.0
|
||||||
'@simplewebauthn/browser': 13.1.0
|
'@simplewebauthn/browser': 13.1.2
|
||||||
'@simplewebauthn/server': 13.1.1
|
'@simplewebauthn/server': 13.1.2
|
||||||
better-call: 1.0.12
|
better-call: 1.0.18
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
jose: 5.10.0
|
jose: 6.1.0
|
||||||
kysely: 0.28.3
|
kysely: 0.28.6
|
||||||
nanostores: 0.11.4
|
nanostores: 0.11.4
|
||||||
zod: 4.0.10
|
zod: 4.1.8
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
better-call@1.0.12:
|
better-call@1.0.18:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-fetch/fetch': 1.1.18
|
'@better-fetch/fetch': 1.1.18
|
||||||
rou3: 0.5.1
|
rou3: 0.5.1
|
||||||
@@ -9875,7 +9878,7 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
jose@5.10.0: {}
|
jose@6.1.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
@@ -9935,7 +9938,7 @@ snapshots:
|
|||||||
|
|
||||||
kolorist@1.8.0: {}
|
kolorist@1.8.0: {}
|
||||||
|
|
||||||
kysely@0.28.3: {}
|
kysely@0.28.6: {}
|
||||||
|
|
||||||
launch-editor@2.10.0:
|
launch-editor@2.10.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12112,8 +12115,8 @@ snapshots:
|
|||||||
compress-commons: 6.0.2
|
compress-commons: 6.0.2
|
||||||
readable-stream: 4.7.0
|
readable-stream: 4.7.0
|
||||||
|
|
||||||
zod-to-json-schema@3.24.5(zod@4.0.10):
|
zod-to-json-schema@3.24.5(zod@4.1.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 4.0.10
|
zod: 4.1.8
|
||||||
|
|
||||||
zod@4.0.10: {}
|
zod@4.1.8: {}
|
||||||
|
|||||||
@@ -8,20 +8,50 @@ import {
|
|||||||
worksCouncilMemberRole,
|
worksCouncilMemberRole,
|
||||||
employeeRole,
|
employeeRole,
|
||||||
adminRole,
|
adminRole,
|
||||||
|
ownerRole,
|
||||||
ROLES,
|
ROLES,
|
||||||
type LegalRole
|
type LegalRole
|
||||||
} from './permissions'
|
} from './permissions'
|
||||||
|
|
||||||
|
const db = new Database('./sqlite.db')
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: new Database('./sqlite.db'),
|
database: db,
|
||||||
onAPIError: { throw: true },
|
onAPIError: { throw: true },
|
||||||
emailAndPassword: { enabled: true, autoSignIn: false },
|
emailAndPassword: { enabled: true, autoSignIn: false, minPasswordLength: 1 },
|
||||||
trustedOrigins: ['http://localhost:3001'],
|
trustedOrigins: ['http://localhost:3001'],
|
||||||
plugins: [
|
plugins: [
|
||||||
jwt({
|
jwt({
|
||||||
jwt: {
|
jwt: {
|
||||||
issuer: 'http://192.168.178.114:3001',
|
issuer: 'http://192.168.178.114:3001',
|
||||||
expirationTime: '1yr'
|
expirationTime: '1yr',
|
||||||
|
definePayload: ({ user, session }) => {
|
||||||
|
let userRoles: string[] = []
|
||||||
|
|
||||||
|
if (session.activeOrganizationId) {
|
||||||
|
try {
|
||||||
|
const roleQuery = db.prepare(`
|
||||||
|
SELECT role
|
||||||
|
FROM member
|
||||||
|
WHERE userId = ? AND organizationId = ?
|
||||||
|
`)
|
||||||
|
const memberRole = roleQuery.get(user.id, session.activeOrganizationId) as { role: string } | undefined
|
||||||
|
|
||||||
|
if (memberRole?.role) {
|
||||||
|
userRoles = [memberRole.role]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error querying user role:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
roles: userRoles,
|
||||||
|
organizationId: session.activeOrganizationId
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
jwks: {
|
jwks: {
|
||||||
keyPairConfig: {
|
keyPairConfig: {
|
||||||
@@ -37,11 +67,10 @@ export const auth = betterAuth({
|
|||||||
[ROLES.EMPLOYER]: employerRole,
|
[ROLES.EMPLOYER]: employerRole,
|
||||||
[ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole,
|
[ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole,
|
||||||
[ROLES.EMPLOYEE]: employeeRole,
|
[ROLES.EMPLOYEE]: employeeRole,
|
||||||
[ROLES.ADMIN]: adminRole
|
[ROLES.ADMIN]: adminRole,
|
||||||
|
[ROLES.OWNER]: ownerRole
|
||||||
},
|
},
|
||||||
|
creatorRole: ROLES.ADMIN, // OWNER fixen here!
|
||||||
// Creator gets admin role by default
|
|
||||||
creatorRole: ROLES.ADMIN,
|
|
||||||
|
|
||||||
async sendInvitationEmail(data) {
|
async sendInvitationEmail(data) {
|
||||||
console.log('Sending invitation email', data)
|
console.log('Sending invitation email', data)
|
||||||
@@ -51,7 +80,8 @@ export const auth = betterAuth({
|
|||||||
[ROLES.EMPLOYER]: 'Arbeitgeber',
|
[ROLES.EMPLOYER]: 'Arbeitgeber',
|
||||||
[ROLES.EMPLOYEE]: 'Arbeitnehmer',
|
[ROLES.EMPLOYEE]: 'Arbeitnehmer',
|
||||||
[ROLES.WORKS_COUNCIL_MEMBER]: 'Betriebsrat',
|
[ROLES.WORKS_COUNCIL_MEMBER]: 'Betriebsrat',
|
||||||
[ROLES.ADMIN]: 'Administrator'
|
[ROLES.ADMIN]: 'Administrator',
|
||||||
|
[ROLES.OWNER]: 'Eigentümer'
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleDisplayName = roleDisplayNames[data.role as LegalRole] || data.role
|
const roleDisplayName = roleDisplayNames[data.role as LegalRole] || data.role
|
||||||
@@ -70,7 +100,7 @@ export const auth = betterAuth({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
throw new Error(`Email sending failed: ${result.error?.statusCode} ${result.error?.error}`)
|
throw new Error(`Email sending failed: ${result.error.message || result.error.name || 'Unknown error'}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Email invite link:', inviteLink)
|
console.log('Email invite link:', inviteLink)
|
||||||
|
|||||||
@@ -91,12 +91,33 @@ export const useOrganizationStore = defineStore('Organization', () => {
|
|||||||
try {
|
try {
|
||||||
await organizationApi.acceptInvitation(invitationId)
|
await organizationApi.acceptInvitation(invitationId)
|
||||||
await navigateTo('/')
|
await navigateTo('/')
|
||||||
|
await syncUserRoleToBackend()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.add({ title: 'Error accepting invitation', color: 'error' })
|
toast.add({ title: 'Error accepting invitation', color: 'error' })
|
||||||
console.error('Error accepting invitation:', e)
|
console.error('Error accepting invitation:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncUserRoleToBackend() {
|
||||||
|
try {
|
||||||
|
const { updateUser } = useUser()
|
||||||
|
const { user } = useAuth()
|
||||||
|
|
||||||
|
if (!user.value?.id) {
|
||||||
|
console.warn('No user ID available for role sync')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call updateUser without userDto to trigger JWT-based sync
|
||||||
|
await updateUser(user.value.id)
|
||||||
|
|
||||||
|
console.log('Successfully synced user role to backend from JWT')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to sync user role to backend from JWT:', error)
|
||||||
|
// Don't throw - role sync failure shouldn't prevent invitation acceptance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function cancelSentInvitation(invitationId: string) {
|
async function cancelSentInvitation(invitationId: string) {
|
||||||
try {
|
try {
|
||||||
await organizationApi.cancelSentInvitation(invitationId)
|
await organizationApi.cancelSentInvitation(invitationId)
|
||||||
|
|||||||
Reference in New Issue
Block a user