feat(fullstack): Add notifications, user is now an entity, add testcontainers, rework custom permissions, get user from JWT in endpoints
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="local-server-backend-h2" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
|
<configuration default="false" name="local-server-backend-h2" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
|
||||||
<option name="ACTIVE_PROFILES" value="local-h2" />
|
<option name="ACTIVE_PROFILES" value="h2" />
|
||||||
<option name="ALTERNATIVE_JRE_PATH" value="ms-21" />
|
<option name="ALTERNATIVE_JRE_PATH" value="ms-21" />
|
||||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||||
<module name="com.betriebsratkanzlei.legalconsenthub.main" />
|
<module name="com.betriebsratkanzlei.legalconsenthub.main" />
|
||||||
|
|||||||
@@ -339,6 +339,34 @@ paths:
|
|||||||
$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"
|
||||||
|
|
||||||
####### Users #######
|
####### Users #######
|
||||||
|
/users:
|
||||||
|
post:
|
||||||
|
summary: Create a new user
|
||||||
|
operationId: createUser
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CreateUserDto"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: User successfully created
|
||||||
|
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"
|
||||||
|
"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"
|
||||||
|
|
||||||
/users/{id}:
|
/users/{id}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
@@ -346,7 +374,6 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
|
||||||
get:
|
get:
|
||||||
summary: Get a specific user
|
summary: Get a specific user
|
||||||
operationId: getUserById
|
operationId: getUserById
|
||||||
@@ -602,6 +629,142 @@ paths:
|
|||||||
"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"
|
||||||
|
|
||||||
|
####### Notifications #######
|
||||||
|
/notifications:
|
||||||
|
get:
|
||||||
|
summary: Get notifications for the current user
|
||||||
|
operationId: getNotifications
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: page
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
description: Page number
|
||||||
|
- in: query
|
||||||
|
name: size
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 20
|
||||||
|
description: Page size
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Paged list of notifications
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PagedNotificationDto"
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
post:
|
||||||
|
summary: Create a new notification
|
||||||
|
operationId: createNotification
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CreateNotificationDto"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Successfully created notification
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/NotificationDto"
|
||||||
|
"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"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
|
||||||
|
/notifications/unread:
|
||||||
|
get:
|
||||||
|
summary: Get unread notifications for the current user
|
||||||
|
operationId: getUnreadNotifications
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of unread notifications
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/NotificationDto"
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
|
||||||
|
/notifications/unread/count:
|
||||||
|
get:
|
||||||
|
summary: Get count of unread notifications for the current user
|
||||||
|
operationId: getUnreadNotificationCount
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Count of unread notifications
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
|
||||||
|
/notifications/mark-all-read:
|
||||||
|
put:
|
||||||
|
summary: Mark all notifications as read for the current user
|
||||||
|
operationId: markAllNotificationsAsRead
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: All notifications marked as read
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
|
||||||
|
/notifications/{id}/mark-read:
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
put:
|
||||||
|
summary: Mark a specific notification as read
|
||||||
|
operationId: markNotificationAsRead
|
||||||
|
tags:
|
||||||
|
- notification
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Notification marked as read
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/NotificationDto"
|
||||||
|
"404":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound"
|
||||||
|
"401":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized"
|
||||||
|
"500":
|
||||||
|
$ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError"
|
||||||
|
|
||||||
####### Files #######
|
####### Files #######
|
||||||
/files:
|
/files:
|
||||||
get:
|
get:
|
||||||
@@ -751,7 +914,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
status:
|
status:
|
||||||
$ref: "#/components/schemas/ApplicationFormStatus"
|
$ref: "#/components/schemas/ApplicationFormStatus"
|
||||||
default: DRAFT
|
|
||||||
|
|
||||||
PagedApplicationFormDto:
|
PagedApplicationFormDto:
|
||||||
type: object
|
type: object
|
||||||
@@ -886,11 +1048,51 @@ components:
|
|||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- name
|
- name
|
||||||
|
- status
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
status:
|
||||||
|
$ref: "#/components/schemas/UserStatus"
|
||||||
|
role:
|
||||||
|
$ref: "#/components/schemas/UserRole"
|
||||||
|
|
||||||
|
CreateUserDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- status
|
||||||
|
- role
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
$ref: "#/components/schemas/UserStatus"
|
||||||
|
role:
|
||||||
|
$ref: "#/components/schemas/UserRole"
|
||||||
|
|
||||||
|
UserStatus:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- INVITED
|
||||||
|
- ACTIVE
|
||||||
|
- BLOCKED
|
||||||
|
- SUSPENDED_SUBSCRIPTION
|
||||||
|
|
||||||
|
UserRole:
|
||||||
|
type: string
|
||||||
|
description: "User's role in the organization"
|
||||||
|
enum:
|
||||||
|
- owner
|
||||||
|
- admin
|
||||||
|
- employer
|
||||||
|
- works_council_member
|
||||||
|
- employee
|
||||||
|
|
||||||
####### CommentDto #######
|
####### CommentDto #######
|
||||||
CommentDto:
|
CommentDto:
|
||||||
@@ -968,6 +1170,85 @@ components:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
####### Notification #######
|
||||||
|
NotificationDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- title
|
||||||
|
- message
|
||||||
|
- clickTarget
|
||||||
|
- isRead
|
||||||
|
- targetGroup
|
||||||
|
- type
|
||||||
|
- createdAt
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
clickTarget:
|
||||||
|
type: string
|
||||||
|
isRead:
|
||||||
|
type: boolean
|
||||||
|
recipient:
|
||||||
|
nullable: true
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/UserDto"
|
||||||
|
targetGroup:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: "#/components/schemas/NotificationType"
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
CreateNotificationDto:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- message
|
||||||
|
- clickTarget
|
||||||
|
- targetGroup
|
||||||
|
- type
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
clickTarget:
|
||||||
|
type: string
|
||||||
|
recipient:
|
||||||
|
nullable: true
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/UserDto"
|
||||||
|
targetGroup:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: "#/components/schemas/NotificationType"
|
||||||
|
|
||||||
|
PagedNotificationDto:
|
||||||
|
type: object
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/Page"
|
||||||
|
required:
|
||||||
|
- content
|
||||||
|
properties:
|
||||||
|
content:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/NotificationDto"
|
||||||
|
|
||||||
|
NotificationType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- INFO
|
||||||
|
- WARNING
|
||||||
|
- ERROR
|
||||||
|
|
||||||
####### FileDto #######
|
####### FileDto #######
|
||||||
FileDto:
|
FileDto:
|
||||||
type: object
|
type: object
|
||||||
@@ -1004,24 +1285,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
|
|
||||||
####### Notification #######
|
|
||||||
NotificationDto:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- id
|
|
||||||
- notificationType
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
notificationType:
|
|
||||||
$ref: "#/components/schemas/NotificationType"
|
|
||||||
|
|
||||||
NotificationType:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- EMAIL
|
|
||||||
|
|
||||||
####### Miscellaneous #######
|
####### Miscellaneous #######
|
||||||
ProcessingPurpose:
|
ProcessingPurpose:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ ext {
|
|||||||
openHtmlVersion = '1.0.10'
|
openHtmlVersion = '1.0.10'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//dependencyManagement {
|
||||||
|
// imports {
|
||||||
|
// mavenBom 'eu.europa.ec.joinup.sd-dss:dss-bom:6.2'
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
|
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-reflect'
|
implementation 'org.jetbrains.kotlin:kotlin-reflect'
|
||||||
@@ -42,8 +48,13 @@ dependencies {
|
|||||||
implementation "com.openhtmltopdf:openhtmltopdf-slf4j:$openHtmlVersion"
|
implementation "com.openhtmltopdf:openhtmltopdf-slf4j:$openHtmlVersion"
|
||||||
implementation "com.openhtmltopdf:openhtmltopdf-svg-support:$openHtmlVersion"
|
implementation "com.openhtmltopdf:openhtmltopdf-svg-support:$openHtmlVersion"
|
||||||
runtimeOnly 'com.h2database:h2'
|
runtimeOnly 'com.h2database:h2'
|
||||||
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-testcontainers'
|
||||||
|
implementation 'org.testcontainers:postgresql'
|
||||||
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
|
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
// implementation 'eu.europa.ec.joinup.sd-dss:dss-validation'
|
||||||
|
// implementation 'eu.europa.ec.joinup.sd-dss:dss-pades'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.betriebsratkanzlei.legalconsenthub.application_form
|
|||||||
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSection
|
||||||
import com.betriebsratkanzlei.legalconsenthub.user.User
|
import com.betriebsratkanzlei.legalconsenthub.user.User
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus
|
||||||
import jakarta.persistence.AttributeOverride
|
import jakarta.persistence.JoinColumn
|
||||||
import jakarta.persistence.AttributeOverrides
|
import jakarta.persistence.ManyToOne
|
||||||
import jakarta.persistence.CascadeType
|
import jakarta.persistence.CascadeType
|
||||||
import jakarta.persistence.Column
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.Entity
|
||||||
@@ -14,7 +14,7 @@ import jakarta.persistence.EnumType
|
|||||||
import jakarta.persistence.GeneratedValue
|
import jakarta.persistence.GeneratedValue
|
||||||
import jakarta.persistence.Id
|
import jakarta.persistence.Id
|
||||||
import jakarta.persistence.OneToMany
|
import jakarta.persistence.OneToMany
|
||||||
import jakarta.persistence.Embedded
|
|
||||||
import org.springframework.data.annotation.CreatedDate
|
import org.springframework.data.annotation.CreatedDate
|
||||||
import org.springframework.data.annotation.LastModifiedDate
|
import org.springframework.data.annotation.LastModifiedDate
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||||
@@ -43,18 +43,12 @@ class ApplicationForm(
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var status: ApplicationFormStatus = ApplicationFormStatus.DRAFT,
|
var status: ApplicationFormStatus = ApplicationFormStatus.DRAFT,
|
||||||
|
|
||||||
@Embedded
|
@ManyToOne
|
||||||
@AttributeOverrides(
|
@JoinColumn(name = "created_by_id", nullable = false)
|
||||||
AttributeOverride(name = "id", column = Column(name = "created_by_id", nullable = false)),
|
|
||||||
AttributeOverride(name = "name", column = Column(name = "created_by_name", nullable = false))
|
|
||||||
)
|
|
||||||
var createdBy: User,
|
var createdBy: User,
|
||||||
|
|
||||||
@Embedded
|
@ManyToOne
|
||||||
@AttributeOverrides(
|
@JoinColumn(name = "last_modified_by_id", nullable = false)
|
||||||
AttributeOverride(name = "id", column = Column(name = "last_modified_by_id", nullable = false)),
|
|
||||||
AttributeOverride(name = "name", column = Column(name = "last_modified_by_name", nullable = false))
|
|
||||||
)
|
|
||||||
var lastModifiedBy: User,
|
var lastModifiedBy: User,
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.application_form
|
package com.betriebsratkanzlei.legalconsenthub.application_form
|
||||||
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSectionMapper
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementSectionMapper
|
||||||
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
|
||||||
import com.betriebsratkanzlei.legalconsenthub.user.User
|
import com.betriebsratkanzlei.legalconsenthub.user.User
|
||||||
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
|
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.user.UserService
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class ApplicationFormMapper(private val formElementSectionMapper: FormElementSectionMapper, private val userMapper: UserMapper) {
|
class ApplicationFormMapper(
|
||||||
|
private val formElementSectionMapper: FormElementSectionMapper,
|
||||||
|
private val userMapper: UserMapper,
|
||||||
|
private val userService: UserService
|
||||||
|
) {
|
||||||
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto {
|
fun toApplicationFormDto(applicationForm: ApplicationForm): ApplicationFormDto {
|
||||||
return ApplicationFormDto(
|
return ApplicationFormDto(
|
||||||
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
|
id = applicationForm.id ?: throw IllegalStateException("ApplicationForm ID must not be null!"),
|
||||||
@@ -43,18 +46,15 @@ class ApplicationFormMapper(private val formElementSectionMapper: FormElementSec
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
fun toApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
||||||
// TODO: Move this in upper layer
|
val currentUser = userService.getCurrentUser()
|
||||||
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
|
||||||
val createdBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
|
|
||||||
val lastModifiedBy = User(principal.name ?: "UNKNOWN USER", principal.id ?: "")
|
|
||||||
|
|
||||||
val applicationForm = ApplicationForm(
|
val applicationForm = ApplicationForm(
|
||||||
name = createApplicationFormDto.name,
|
name = createApplicationFormDto.name,
|
||||||
isTemplate = createApplicationFormDto.isTemplate,
|
isTemplate = createApplicationFormDto.isTemplate,
|
||||||
organizationId = createApplicationFormDto.organizationId ?: "",
|
organizationId = createApplicationFormDto.organizationId ?: "",
|
||||||
status = createApplicationFormDto.status ?: com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus.DRAFT,
|
status = createApplicationFormDto.status ?: com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus.DRAFT,
|
||||||
createdBy = createdBy,
|
createdBy = currentUser,
|
||||||
lastModifiedBy = lastModifiedBy,
|
lastModifiedBy = currentUser,
|
||||||
)
|
)
|
||||||
applicationForm.formElementSections = createApplicationFormDto.formElementSections
|
applicationForm.formElementSections = createApplicationFormDto.formElementSections
|
||||||
.map { formElementSectionMapper.toFormElementSection(it, applicationForm) }
|
.map { formElementSectionMapper.toFormElementSection(it, applicationForm) }
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ 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.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
|
||||||
@@ -16,7 +18,8 @@ import java.util.UUID
|
|||||||
@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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm {
|
||||||
@@ -76,10 +79,61 @@ class ApplicationFormService(
|
|||||||
|
|
||||||
applicationForm.status = ApplicationFormStatus.SUBMITTED
|
applicationForm.status = ApplicationFormStatus.SUBMITTED
|
||||||
|
|
||||||
return try {
|
val savedApplicationForm = try {
|
||||||
applicationFormRepository.save(applicationForm)
|
applicationFormRepository.save(applicationForm)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw ApplicationFormNotUpdatedException(e, id)
|
throw ApplicationFormNotUpdatedException(e, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create notifications for relevant users
|
||||||
|
createSubmissionNotifications(savedApplicationForm)
|
||||||
|
|
||||||
|
return savedApplicationForm
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSubmissionNotifications(applicationForm: ApplicationForm) {
|
||||||
|
val title = "Neuer Mitbestimmungsantrag eingereicht"
|
||||||
|
val message = "Ein neuer Mitbestimmungsantrag '${applicationForm.name}' wurde von ${applicationForm.createdBy.name} eingereicht und wartet auf Ihre Bearbeitung."
|
||||||
|
val clickTarget = "/application-forms/${applicationForm.id}/0"
|
||||||
|
|
||||||
|
// Create notification for admin users
|
||||||
|
notificationService.createNotificationForUser(
|
||||||
|
title = title,
|
||||||
|
message = message,
|
||||||
|
clickTarget = clickTarget,
|
||||||
|
recipient = null,
|
||||||
|
targetGroup = "admin",
|
||||||
|
type = NotificationType.INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create notification for works council members
|
||||||
|
notificationService.createNotificationForUser(
|
||||||
|
title = title,
|
||||||
|
message = message,
|
||||||
|
clickTarget = clickTarget,
|
||||||
|
recipient = null,
|
||||||
|
targetGroup = "works_council_member",
|
||||||
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ package com.betriebsratkanzlei.legalconsenthub.comment;
|
|||||||
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm
|
import com.betriebsratkanzlei.legalconsenthub.application_form.ApplicationForm
|
||||||
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElement
|
||||||
import com.betriebsratkanzlei.legalconsenthub.user.User
|
import com.betriebsratkanzlei.legalconsenthub.user.User
|
||||||
import jakarta.persistence.AttributeOverride
|
|
||||||
import jakarta.persistence.AttributeOverrides
|
|
||||||
import jakarta.persistence.Column
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.Embedded
|
|
||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.Entity
|
||||||
import jakarta.persistence.EntityListeners
|
import jakarta.persistence.EntityListeners
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
@@ -30,11 +27,8 @@ class Comment(
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var message: String = "",
|
var message: String = "",
|
||||||
|
|
||||||
@Embedded
|
@ManyToOne
|
||||||
@AttributeOverrides(
|
@JoinColumn(name = "created_by_id", nullable = false)
|
||||||
AttributeOverride(name = "id", column = Column(name = "created_by_id", nullable = false)),
|
|
||||||
AttributeOverride(name = "name", column = Column(name = "created_by_name", nullable = false))
|
|
||||||
)
|
|
||||||
var createdBy: User,
|
var createdBy: User,
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundExcep
|
|||||||
import com.betriebsratkanzlei.legalconsenthub.error.FormElementNotFoundException
|
import com.betriebsratkanzlei.legalconsenthub.error.FormElementNotFoundException
|
||||||
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementRepository
|
import com.betriebsratkanzlei.legalconsenthub.form_element.FormElementRepository
|
||||||
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
|
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.user.UserService
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CommentDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CommentDto
|
||||||
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateCommentDto
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateCommentDto
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
@@ -14,6 +15,7 @@ import java.util.UUID
|
|||||||
@Component
|
@Component
|
||||||
class CommentMapper(
|
class CommentMapper(
|
||||||
private val userMapper: UserMapper,
|
private val userMapper: UserMapper,
|
||||||
|
private val userService: UserService,
|
||||||
private val applicationFormRepository: ApplicationFormRepository,
|
private val applicationFormRepository: ApplicationFormRepository,
|
||||||
private val formElementRepository: FormElementRepository
|
private val formElementRepository: FormElementRepository
|
||||||
) {
|
) {
|
||||||
@@ -53,9 +55,11 @@ class CommentMapper(
|
|||||||
val formElement = formElementRepository.findById(formElementId)
|
val formElement = formElementRepository.findById(formElementId)
|
||||||
.orElseThrow { FormElementNotFoundException(formElementId) }
|
.orElseThrow { FormElementNotFoundException(formElementId) }
|
||||||
|
|
||||||
|
val currentUser = userService.getCurrentUser()
|
||||||
|
|
||||||
return Comment(
|
return Comment(
|
||||||
message = commentDto.message,
|
message = commentDto.message,
|
||||||
createdBy = userMapper.toUser(commentDto.createdBy),
|
createdBy = currentUser,
|
||||||
applicationForm = applicationForm,
|
applicationForm = applicationForm,
|
||||||
formElement = formElement
|
formElement = formElement
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm
|
|||||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
import org.springframework.security.oauth2.jwt.JwtDecoder
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
|
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@@ -24,6 +25,8 @@ class SecurityConfig {
|
|||||||
authorizeHttpRequests {
|
authorizeHttpRequests {
|
||||||
authorize("/swagger-ui/**", permitAll)
|
authorize("/swagger-ui/**", permitAll)
|
||||||
authorize("/v3/**", permitAll)
|
authorize("/v3/**", permitAll)
|
||||||
|
// For user registration
|
||||||
|
authorize(HttpMethod.POST, "/users", permitAll)
|
||||||
authorize(anyRequest, authenticated)
|
authorize(anyRequest, authenticated)
|
||||||
}
|
}
|
||||||
oauth2ResourceServer {
|
oauth2ResourceServer {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.config
|
||||||
|
|
||||||
|
import com.github.dockerjava.api.model.ExposedPort
|
||||||
|
import com.github.dockerjava.api.model.HostConfig
|
||||||
|
import com.github.dockerjava.api.model.PortBinding
|
||||||
|
import com.github.dockerjava.api.model.Ports
|
||||||
|
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.context.annotation.Profile
|
||||||
|
import org.testcontainers.containers.PostgreSQLContainer;
|
||||||
|
import org.testcontainers.utility.DockerImageName
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Profile("testcontainers")
|
||||||
|
class TestContainersConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ServiceConnection
|
||||||
|
fun postgresContainer(): PostgreSQLContainer<*> {
|
||||||
|
return PostgreSQLContainer(DockerImageName.parse("postgres:17-alpine"))
|
||||||
|
.withDatabaseName("legalconsenthub")
|
||||||
|
.withUsername("legalconsenthub")
|
||||||
|
.withPassword("legalconsenthub")
|
||||||
|
.withExposedPorts(5432)
|
||||||
|
.withCreateContainerCmdModifier { cmd ->
|
||||||
|
cmd.withHostConfig(
|
||||||
|
HostConfig().apply {
|
||||||
|
this.withPortBindings(
|
||||||
|
PortBinding(
|
||||||
|
Ports.Binding.bindPort(5432),
|
||||||
|
ExposedPort(5432)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.withReuse(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ class ExceptionHandler {
|
|||||||
var logger = LoggerFactory.getLogger(ExceptionHandler::class.java)
|
var logger = LoggerFactory.getLogger(ExceptionHandler::class.java)
|
||||||
|
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@ExceptionHandler(ApplicationFormNotFoundException::class)
|
@ExceptionHandler(ApplicationFormNotFoundException::class, UserNotFoundException::class)
|
||||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
fun handleNotFoundError(e: Exception): ResponseEntity<ProblemDetails> {
|
fun handleNotFoundError(e: Exception): ResponseEntity<ProblemDetails> {
|
||||||
logger.warn(e.message, e)
|
logger.warn(e.message, e)
|
||||||
@@ -47,6 +47,22 @@ class ExceptionHandler {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@ExceptionHandler(UserAlreadyExistsException::class)
|
||||||
|
@ResponseStatus(HttpStatus.CONFLICT)
|
||||||
|
fun handleUserAlreadyExistsError(e: UserAlreadyExistsException): ResponseEntity<ProblemDetails> {
|
||||||
|
logger.warn(e.message, e)
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT).contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||||
|
.body(
|
||||||
|
ProblemDetails(
|
||||||
|
title = "Conflict",
|
||||||
|
status = HttpStatus.CONFLICT.value(),
|
||||||
|
type = URI.create("about:blank"),
|
||||||
|
detail = e.message ?: "Resource already exists"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@ExceptionHandler(
|
@ExceptionHandler(
|
||||||
ApplicationFormNotCreatedException::class,
|
ApplicationFormNotCreatedException::class,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.error
|
||||||
|
|
||||||
|
class UserAlreadyExistsException(id: String): RuntimeException("User with ID $id already exists")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.error
|
||||||
|
|
||||||
|
class UserNotFoundException(id: String): RuntimeException("Couldn't find user with ID: $id")
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.user.User
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.JoinColumn
|
||||||
|
import jakarta.persistence.ManyToOne
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.EntityListeners
|
||||||
|
import jakarta.persistence.Enumerated
|
||||||
|
import jakarta.persistence.EnumType
|
||||||
|
import jakarta.persistence.GeneratedValue
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import org.springframework.data.annotation.CreatedDate
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@EntityListeners(AuditingEntityListener::class)
|
||||||
|
class Notification(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
var id: UUID? = null,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var title: String = "",
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
|
var message: String = "",
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var clickTarget: String = "",
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var isRead: Boolean = false,
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "recipient_id", nullable = true)
|
||||||
|
var recipient: User?,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var targetGroup: String = "",
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
var type: NotificationType = NotificationType.INFO,
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(nullable = false)
|
||||||
|
var createdAt: LocalDateTime? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.user.UserService
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class NotificationController(
|
||||||
|
private val notificationService: NotificationService,
|
||||||
|
private val notificationMapper: NotificationMapper,
|
||||||
|
private val pagedNotificationMapper: PagedNotificationMapper,
|
||||||
|
private val userService: UserService
|
||||||
|
) : NotificationApi {
|
||||||
|
|
||||||
|
override fun getNotifications(page: Int, size: Int): ResponseEntity<PagedNotificationDto> {
|
||||||
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
|
val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
|
||||||
|
|
||||||
|
val user = userService.getUserById(recipientId)
|
||||||
|
|
||||||
|
val notifications = if (user.role != null) {
|
||||||
|
notificationService.getNotificationsForUserAndGroup(
|
||||||
|
recipientId = recipientId,
|
||||||
|
userRole = user.role!!.value,
|
||||||
|
page = page,
|
||||||
|
size = size
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
notificationService.getNotificationsForUser(
|
||||||
|
recipientId = recipientId,
|
||||||
|
page = page,
|
||||||
|
size = size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUnreadNotifications(): ResponseEntity<List<NotificationDto>> {
|
||||||
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
|
val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
|
||||||
|
|
||||||
|
val user = userService.getUserById(recipientId)
|
||||||
|
|
||||||
|
val notifications = if (user.role != null) {
|
||||||
|
notificationService.getUnreadNotificationsForUserAndGroup(recipientId, user.role!!.value)
|
||||||
|
} else {
|
||||||
|
notificationService.getUnreadNotificationsForUser(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUnreadNotificationCount(): ResponseEntity<Long> {
|
||||||
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
|
val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
|
||||||
|
|
||||||
|
val user = userService.getUserById(recipientId)
|
||||||
|
|
||||||
|
val count = if (user.role != null) {
|
||||||
|
notificationService.getUnreadNotificationCountForUserAndGroup(recipientId, user.role!!.value)
|
||||||
|
} else {
|
||||||
|
notificationService.getUnreadNotificationCount(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun markAllNotificationsAsRead(): ResponseEntity<Unit> {
|
||||||
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
|
val recipientId = principal.id ?: throw IllegalStateException("User ID not found")
|
||||||
|
|
||||||
|
val user = userService.getUserById(recipientId)
|
||||||
|
|
||||||
|
if (user.role != null) {
|
||||||
|
notificationService.markAllAsReadForUserAndGroup(recipientId, user.role!!.value)
|
||||||
|
} else {
|
||||||
|
notificationService.markAllAsRead(recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.noContent().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun markNotificationAsRead(id: UUID): ResponseEntity<NotificationDto> {
|
||||||
|
val notification = notificationService.markAsRead(id)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
|
||||||
|
return ResponseEntity.ok(notificationMapper.toNotificationDto(notification))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createNotification(createNotificationDto: CreateNotificationDto): ResponseEntity<NotificationDto> {
|
||||||
|
val notification = notificationService.createNotification(createNotificationDto)
|
||||||
|
return ResponseEntity.ok(notificationMapper.toNotificationDto(notification))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.user.UserMapper
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class NotificationMapper(
|
||||||
|
private val userMapper: UserMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toNotificationDto(notification: Notification): NotificationDto {
|
||||||
|
return NotificationDto(
|
||||||
|
id = notification.id!!,
|
||||||
|
title = notification.title,
|
||||||
|
message = notification.message,
|
||||||
|
clickTarget = notification.clickTarget,
|
||||||
|
isRead = notification.isRead,
|
||||||
|
recipient = notification.recipient?.let { userMapper.toUserDto(it) },
|
||||||
|
targetGroup = notification.targetGroup,
|
||||||
|
type = notification.type,
|
||||||
|
createdAt = notification.createdAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toNotification(createNotificationDto: CreateNotificationDto): Notification {
|
||||||
|
return Notification(
|
||||||
|
title = createNotificationDto.title,
|
||||||
|
message = createNotificationDto.message,
|
||||||
|
clickTarget = createNotificationDto.clickTarget,
|
||||||
|
isRead = false,
|
||||||
|
recipient = createNotificationDto.recipient?.let { userMapper.toUser(it) },
|
||||||
|
targetGroup = createNotificationDto.targetGroup,
|
||||||
|
type = createNotificationDto.type
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Modifying
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.data.repository.query.Param
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface NotificationRepository : JpaRepository<Notification, UUID> {
|
||||||
|
|
||||||
|
fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page<Notification>
|
||||||
|
|
||||||
|
fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List<Notification>
|
||||||
|
|
||||||
|
fun findByRecipientIsNullOrderByCreatedAtDesc(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 n.targetGroup = :targetGroup) OR (n.recipient IS NULL AND n.targetGroup = 'all') ORDER BY n.createdAt DESC")
|
||||||
|
fun findByRecipientIdOrTargetGroupOrderByCreatedAtDesc(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String, pageable: Pageable): Page<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')) AND n.isRead = false ORDER BY n.createdAt DESC")
|
||||||
|
fun findByRecipientIdOrTargetGroupAndIsReadFalseOrderByCreatedAtDesc(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String): List<Notification>
|
||||||
|
|
||||||
|
@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")
|
||||||
|
fun countByRecipientIdOrTargetGroupAndIsReadFalse(@Param("recipientId") recipientId: String, @Param("targetGroup") targetGroup: String): Long
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :recipientId")
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.user.User
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType
|
||||||
|
import org.springframework.data.domain.Page
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class NotificationService(
|
||||||
|
private val notificationRepository: NotificationRepository,
|
||||||
|
private val notificationMapper: NotificationMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun createNotification(createNotificationDto: CreateNotificationDto): Notification {
|
||||||
|
val notification = notificationMapper.toNotification(createNotificationDto)
|
||||||
|
return notificationRepository.save(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNotificationForUser(
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
clickTarget: String,
|
||||||
|
recipient: User?,
|
||||||
|
targetGroup: String,
|
||||||
|
type: NotificationType = NotificationType.INFO
|
||||||
|
): Notification {
|
||||||
|
val notification = Notification(
|
||||||
|
title = title,
|
||||||
|
message = message,
|
||||||
|
clickTarget = clickTarget,
|
||||||
|
recipient = recipient,
|
||||||
|
targetGroup = targetGroup,
|
||||||
|
type = type
|
||||||
|
)
|
||||||
|
return notificationRepository.save(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNotificationsForUser(recipientId: String, page: Int = 0, size: Int = 20): Page<Notification> {
|
||||||
|
val pageable = PageRequest.of(page, size)
|
||||||
|
return notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, 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 {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.notification
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto
|
||||||
|
import org.springframework.data.domain.Page
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class PagedNotificationMapper(
|
||||||
|
private val notificationMapper: NotificationMapper
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toPagedNotificationDto(page: Page<Notification>): PagedNotificationDto {
|
||||||
|
return PagedNotificationDto(
|
||||||
|
content = page.content.map { notificationMapper.toNotificationDto(it) },
|
||||||
|
number = page.number,
|
||||||
|
propertySize = page.size,
|
||||||
|
numberOfElements = page.numberOfElements,
|
||||||
|
totalElements = page.totalElements,
|
||||||
|
totalPages = page.totalPages,
|
||||||
|
first = page.isFirst,
|
||||||
|
last = page.isLast,
|
||||||
|
empty = page.isEmpty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,37 @@
|
|||||||
package com.betriebsratkanzlei.legalconsenthub.user
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
import jakarta.persistence.Embeddable
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserRole
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import org.springframework.data.annotation.CreatedDate
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Embeddable
|
@Entity
|
||||||
|
@EntityListeners(AuditingEntityListener::class)
|
||||||
|
@Table(name = "app_user")
|
||||||
class User(
|
class User(
|
||||||
|
@Id
|
||||||
|
@Column(nullable = false)
|
||||||
|
var id: String,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
var name: String,
|
var name: String,
|
||||||
var id: String
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
var status: UserStatus = UserStatus.ACTIVE,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = true)
|
||||||
|
var role: UserRole? = null,
|
||||||
|
|
||||||
|
@CreatedDate
|
||||||
|
@Column(nullable = false)
|
||||||
|
var createdAt: LocalDateTime? = null,
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(nullable = false)
|
||||||
|
var modifiedAt: LocalDateTime? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.api.UserApi
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class UserController(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val userMapper: UserMapper
|
||||||
|
) : UserApi {
|
||||||
|
|
||||||
|
override fun createUser(createUserDto: CreateUserDto): ResponseEntity<UserDto> {
|
||||||
|
val user = userService.createUser(createUserDto)
|
||||||
|
return ResponseEntity.status(201).body(userMapper.toUserDto(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUserById(id: String): ResponseEntity<UserDto> {
|
||||||
|
val user = userService.getUserById(id)
|
||||||
|
return ResponseEntity.ok(userMapper.toUserDto(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteUser(id: String): ResponseEntity<Unit> {
|
||||||
|
userService.deleteUser(id)
|
||||||
|
return ResponseEntity.noContent().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ class UserMapper() {
|
|||||||
return UserDto(
|
return UserDto(
|
||||||
id = user.id,
|
id = user.id,
|
||||||
name = user.name,
|
name = user.name,
|
||||||
|
status = user.status,
|
||||||
|
role = user.role
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +18,8 @@ class UserMapper() {
|
|||||||
return User(
|
return User(
|
||||||
id = userDto.id,
|
id = userDto.id,
|
||||||
name = userDto.name,
|
name = userDto.name,
|
||||||
|
status = userDto.status,
|
||||||
|
role = userDto.role
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface UserRepository : JpaRepository<User, String>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.betriebsratkanzlei.legalconsenthub.user
|
||||||
|
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.error.UserAlreadyExistsException
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.error.UserNotFoundException
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.CreateUserDto
|
||||||
|
import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class UserService(
|
||||||
|
private val userRepository: UserRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getCurrentUser(): User {
|
||||||
|
val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal
|
||||||
|
val userId = principal.id ?: throw IllegalStateException("User ID not found")
|
||||||
|
|
||||||
|
return userRepository.findById(userId)
|
||||||
|
.orElseThrow { UserNotFoundException(userId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createUser(createUserDto: CreateUserDto): User {
|
||||||
|
if (userRepository.existsById(createUserDto.id)) {
|
||||||
|
throw UserAlreadyExistsException(createUserDto.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
val user = User(
|
||||||
|
id = createUserDto.id,
|
||||||
|
name = createUserDto.name,
|
||||||
|
status = createUserDto.status ?: UserStatus.ACTIVE,
|
||||||
|
role = createUserDto.role
|
||||||
|
)
|
||||||
|
return userRepository.save(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUserById(userId: String): User {
|
||||||
|
return userRepository.findById(userId)
|
||||||
|
.orElseThrow { UserNotFoundException(userId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteUser(userId: String) {
|
||||||
|
userRepository.deleteById(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: legalconsenthub
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: create
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
jdbc:
|
||||||
|
batch_size: 100
|
||||||
|
order_inserts: true
|
||||||
|
enable_lazy_load_no_trans: true
|
||||||
|
|
||||||
|
liquibase:
|
||||||
|
enabled: true
|
||||||
|
drop-first: false
|
||||||
|
change-log: classpath:/db/changelog/db.changelog-master.yaml
|
||||||
|
default-schema: public
|
||||||
|
|
||||||
|
sql:
|
||||||
|
init:
|
||||||
|
platform: postgresql
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org:
|
||||||
|
springframework:
|
||||||
|
security: TRACE
|
||||||
|
oauth2: TRACE
|
||||||
|
org.testcontainers: INFO
|
||||||
|
com.github.dockerjava: WARN
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
create table app_user
|
||||||
|
(
|
||||||
|
created_at timestamp(6) not null,
|
||||||
|
modified_at timestamp(6) not null,
|
||||||
|
id 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')),
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
create table application_form
|
create table application_form
|
||||||
(
|
(
|
||||||
is_template boolean not null,
|
is_template boolean not null,
|
||||||
@@ -5,12 +16,10 @@ create table application_form
|
|||||||
modified_at timestamp(6) not null,
|
modified_at timestamp(6) not null,
|
||||||
id uuid not null,
|
id uuid not null,
|
||||||
created_by_id varchar(255) not null,
|
created_by_id varchar(255) not null,
|
||||||
created_by_name varchar(255) not null,
|
|
||||||
last_modified_by_id varchar(255) not null,
|
last_modified_by_id varchar(255) not null,
|
||||||
last_modified_by_name varchar(255) not null,
|
|
||||||
name varchar(255) not null,
|
name varchar(255) not null,
|
||||||
organization_id varchar(255),
|
organization_id varchar(255),
|
||||||
status enum ('APPROVED','DRAFT','REJECTED','SIGNED','SUBMITTED') not null,
|
status varchar(255) not null check (status in ('DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED', 'SIGNED')),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -22,15 +31,14 @@ create table comment
|
|||||||
form_element_id uuid not null,
|
form_element_id uuid not null,
|
||||||
id uuid not null,
|
id uuid not null,
|
||||||
created_by_id varchar(255) not null,
|
created_by_id varchar(255) not null,
|
||||||
created_by_name varchar(255) not null,
|
|
||||||
message varchar(255) not null,
|
message varchar(255) not null,
|
||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table form_element_options
|
create table form_element_options
|
||||||
(
|
(
|
||||||
employee_data_category tinyint not null check (employee_data_category between 0 and 3),
|
employee_data_category smallint not null check (employee_data_category between 0 and 3),
|
||||||
processing_purpose tinyint not null check (processing_purpose between 0 and 3),
|
processing_purpose smallint not null check (processing_purpose between 0 and 3),
|
||||||
form_element_id uuid not null,
|
form_element_id uuid not null,
|
||||||
label varchar(255) not null,
|
label varchar(255) not null,
|
||||||
option_value varchar(255) not null
|
option_value varchar(255) not null
|
||||||
@@ -38,7 +46,7 @@ create table form_element_options
|
|||||||
|
|
||||||
create table form_element
|
create table form_element
|
||||||
(
|
(
|
||||||
type tinyint not null check (type between 0 and 4),
|
type smallint not null check (type between 0 and 4),
|
||||||
form_element_section_id uuid not null,
|
form_element_section_id uuid not null,
|
||||||
id uuid not null,
|
id uuid not null,
|
||||||
description varchar(255),
|
description varchar(255),
|
||||||
@@ -56,11 +64,40 @@ create table form_element_section
|
|||||||
primary key (id)
|
primary key (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table notification
|
||||||
|
(
|
||||||
|
is_read boolean not null,
|
||||||
|
created_at timestamp(6) not null,
|
||||||
|
id uuid not null,
|
||||||
|
click_target varchar(255) not null,
|
||||||
|
message TEXT not null,
|
||||||
|
recipient_id varchar(255),
|
||||||
|
target_group varchar(255) not null,
|
||||||
|
title varchar(255) not null,
|
||||||
|
type varchar(255) not null check (type in ('INFO', 'WARNING', 'ERROR')),
|
||||||
|
primary key (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table if exists application_form
|
||||||
|
add constraint FKhtad5onoy2jknhtyfmx6cvvey
|
||||||
|
foreign key (created_by_id)
|
||||||
|
references app_user;
|
||||||
|
|
||||||
|
alter table if exists application_form
|
||||||
|
add constraint FK5yewx8bespw0uiivxioeh7q0d
|
||||||
|
foreign key (last_modified_by_id)
|
||||||
|
references app_user;
|
||||||
|
|
||||||
alter table if exists comment
|
alter table if exists comment
|
||||||
add constraint FKlavy9axrt26sepreg5lqtuoap
|
add constraint FKlavy9axrt26sepreg5lqtuoap
|
||||||
foreign key (application_form_id)
|
foreign key (application_form_id)
|
||||||
references application_form;
|
references application_form;
|
||||||
|
|
||||||
|
alter table if exists comment
|
||||||
|
add constraint FKbbjqikfmgeacfsnaasxxqoygh
|
||||||
|
foreign key (created_by_id)
|
||||||
|
references app_user;
|
||||||
|
|
||||||
alter table if exists comment
|
alter table if exists comment
|
||||||
add constraint FKfg84w0i76tw9os13950272c6f
|
add constraint FKfg84w0i76tw9os13950272c6f
|
||||||
foreign key (form_element_id)
|
foreign key (form_element_id)
|
||||||
@@ -80,3 +117,8 @@ alter table if exists form_element_section
|
|||||||
add constraint FKtn0lreovauwf2v29doo70o3qs
|
add constraint FKtn0lreovauwf2v29doo70o3qs
|
||||||
foreign key (application_form_id)
|
foreign key (application_form_id)
|
||||||
references application_form;
|
references application_form;
|
||||||
|
|
||||||
|
alter table if exists notification
|
||||||
|
add constraint FKeg1j4hnp0y4lbm0y35hgr4e8r
|
||||||
|
foreign key (recipient_id)
|
||||||
|
references app_user;
|
||||||
|
|||||||
89
legalconsenthub/components/NotificationsSlideover.vue
Normal file
89
legalconsenthub/components/NotificationsSlideover.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<USlideover v-model:open="isOpen" title="Benachrichtigungen">
|
||||||
|
<template #body>
|
||||||
|
<div v-if="notifications.length === 0" class="text-center py-8 text-muted">
|
||||||
|
<UIcon name="i-heroicons-bell-slash" class="h-8 w-8 mx-auto mb-2" />
|
||||||
|
<p>Keine Benachrichtigungen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
:to="notification.clickTarget"
|
||||||
|
class="px-3 py-2.5 rounded-md hover:bg-elevated/50 flex items-center gap-3 relative -mx-3 first:-mt-3 last:-mb-3"
|
||||||
|
@click="handleNotificationClick(notification)"
|
||||||
|
>
|
||||||
|
<UChip
|
||||||
|
:color="notification.type === 'ERROR' ? 'error' : notification.type === 'WARNING' ? 'warning' : 'primary'"
|
||||||
|
:show="!notification.isRead"
|
||||||
|
inset
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
:name="
|
||||||
|
notification.type === 'ERROR'
|
||||||
|
? 'i-heroicons-x-circle'
|
||||||
|
: notification.type === 'WARNING'
|
||||||
|
? 'i-heroicons-exclamation-triangle'
|
||||||
|
: 'i-heroicons-information-circle'
|
||||||
|
"
|
||||||
|
class="h-6 w-6"
|
||||||
|
/>
|
||||||
|
</UChip>
|
||||||
|
|
||||||
|
<div class="text-sm flex-1">
|
||||||
|
<p class="flex items-center justify-between">
|
||||||
|
<span class="text-highlighted font-medium">{{ notification.title }}</span>
|
||||||
|
|
||||||
|
<time
|
||||||
|
:datetime="notification.createdAt.toISOString()"
|
||||||
|
class="text-muted text-xs"
|
||||||
|
v-text="formatTimeAgo(notification.createdAt)"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-dimmed">
|
||||||
|
{{ notification.message }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<UBadge
|
||||||
|
:color="notification.type === 'ERROR' ? 'error' : notification.type === 'WARNING' ? 'warning' : 'info'"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ notification.type }}
|
||||||
|
</UBadge>
|
||||||
|
<UBadge color="neutral" variant="subtle" size="xs">
|
||||||
|
{{ notification.targetGroup }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</USlideover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatTimeAgo } from '@vueuse/core'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { notifications, fetchNotifications, handleNotificationClick } = useNotification()
|
||||||
|
|
||||||
|
watch(isOpen, async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
await fetchNotifications()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
export { useApplicationFormTemplate } from './applicationFormTemplate/useApplicationFormTemplate'
|
export { useApplicationFormTemplate } from './applicationFormTemplate/useApplicationFormTemplate'
|
||||||
export { useApplicationForm } from './applicationForm/useApplicationForm'
|
export { useApplicationForm } from './applicationForm/useApplicationForm'
|
||||||
|
export { useNotification } from './notification/useNotification'
|
||||||
|
export { useNotificationApi } from './notification/useNotificationApi'
|
||||||
|
export { useUser } from './user/useUser'
|
||||||
|
export { useUserApi } from './user/useUserApi'
|
||||||
|
|||||||
118
legalconsenthub/composables/notification/useNotification.ts
Normal file
118
legalconsenthub/composables/notification/useNotification.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import type { NotificationDto } from '~/.api-client'
|
||||||
|
|
||||||
|
export const useNotification = () => {
|
||||||
|
const {
|
||||||
|
getNotifications,
|
||||||
|
getUnreadNotifications,
|
||||||
|
getUnreadNotificationCount,
|
||||||
|
markAllNotificationsAsRead,
|
||||||
|
markNotificationAsRead
|
||||||
|
} = useNotificationApi()
|
||||||
|
|
||||||
|
const notifications = ref<NotificationDto[]>([])
|
||||||
|
const unreadNotifications = ref<NotificationDto[]>([])
|
||||||
|
const unreadCount = ref<number>(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
const fetchNotifications = async (page: number = 0, size: number = 20) => {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await getNotifications(page, size)
|
||||||
|
notifications.value = response.content || []
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch notifications:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUnreadNotifications = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getUnreadNotifications()
|
||||||
|
unreadNotifications.value = response || []
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch unread notifications:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUnreadCount = async () => {
|
||||||
|
try {
|
||||||
|
const count = await getUnreadNotificationCount()
|
||||||
|
unreadCount.value = count || 0
|
||||||
|
return count
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch unread count:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const markAllAsRead = async () => {
|
||||||
|
try {
|
||||||
|
await markAllNotificationsAsRead()
|
||||||
|
unreadCount.value = 0
|
||||||
|
unreadNotifications.value = []
|
||||||
|
notifications.value = notifications.value.map((n) => ({ ...n, isRead: true }))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark all as read:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const markAsRead = async (notificationId: string) => {
|
||||||
|
try {
|
||||||
|
await markNotificationAsRead(notificationId)
|
||||||
|
const index = notifications.value.findIndex((n) => n.id === notificationId)
|
||||||
|
if (index !== -1) {
|
||||||
|
notifications.value[index].isRead = true
|
||||||
|
}
|
||||||
|
// Remove from unread notifications
|
||||||
|
unreadNotifications.value = unreadNotifications.value.filter((n) => n.id !== notificationId)
|
||||||
|
|
||||||
|
if (unreadCount.value > 0) {
|
||||||
|
unreadCount.value--
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark notification as read:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNotificationClick = async (notification: NotificationDto) => {
|
||||||
|
if (!notification.isRead) {
|
||||||
|
await markAsRead(notification.id)
|
||||||
|
}
|
||||||
|
if (notification.clickTarget) {
|
||||||
|
await navigateTo(notification.clickTarget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPeriodicRefresh = (intervalMs: number = 30000) => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchUnreadCount()
|
||||||
|
}, intervalMs)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(interval)
|
||||||
|
})
|
||||||
|
|
||||||
|
return interval
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
notifications: readonly(notifications),
|
||||||
|
unreadNotifications: readonly(unreadNotifications),
|
||||||
|
unreadCount: readonly(unreadCount),
|
||||||
|
isLoading: readonly(isLoading),
|
||||||
|
fetchNotifications,
|
||||||
|
fetchUnreadNotifications,
|
||||||
|
fetchUnreadCount,
|
||||||
|
markAllAsRead,
|
||||||
|
markAsRead,
|
||||||
|
handleNotificationClick,
|
||||||
|
startPeriodicRefresh
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
NotificationApi,
|
||||||
|
Configuration,
|
||||||
|
type NotificationDto,
|
||||||
|
type PagedNotificationDto,
|
||||||
|
type CreateNotificationDto
|
||||||
|
} from '~/.api-client'
|
||||||
|
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
||||||
|
|
||||||
|
export function useNotificationApi() {
|
||||||
|
const appBaseUrl = useRuntimeConfig().app.baseURL
|
||||||
|
const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public
|
||||||
|
const { jwt } = useAuth()
|
||||||
|
|
||||||
|
const basePath = withoutTrailingSlash(
|
||||||
|
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
|
||||||
|
)
|
||||||
|
|
||||||
|
const notificationApiClient = new NotificationApi(
|
||||||
|
new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })
|
||||||
|
)
|
||||||
|
|
||||||
|
async function createNotification(createNotificationDto: CreateNotificationDto): Promise<NotificationDto> {
|
||||||
|
return notificationApiClient.createNotification({ createNotificationDto })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNotifications(page?: number, size?: number): Promise<PagedNotificationDto> {
|
||||||
|
return notificationApiClient.getNotifications({ page, size })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUnreadNotifications(): Promise<NotificationDto[]> {
|
||||||
|
return notificationApiClient.getUnreadNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUnreadNotificationCount(): Promise<number> {
|
||||||
|
return notificationApiClient.getUnreadNotificationCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markAllNotificationsAsRead(): Promise<void> {
|
||||||
|
return notificationApiClient.markAllNotificationsAsRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markNotificationAsRead(id: string): Promise<NotificationDto> {
|
||||||
|
return notificationApiClient.markNotificationAsRead({ id })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createNotification,
|
||||||
|
getNotifications,
|
||||||
|
getUnreadNotifications,
|
||||||
|
getUnreadNotificationCount,
|
||||||
|
markAllNotificationsAsRead,
|
||||||
|
markNotificationAsRead
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
worksCouncilMemberRole,
|
worksCouncilMemberRole,
|
||||||
employeeRole,
|
employeeRole,
|
||||||
adminRole,
|
adminRole,
|
||||||
|
ownerRole,
|
||||||
ROLES
|
ROLES
|
||||||
} from '~/server/utils/permissions'
|
} from '~/server/utils/permissions'
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ export function useAuth() {
|
|||||||
[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
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
jwtClient()
|
jwtClient()
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ export function usePermissions() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const canInviteMembers = computed(() =>
|
const canInviteMembers = computed(() =>
|
||||||
hasPermission({ member: ["invite"] })
|
hasPermission({ invitation: ["create"] })
|
||||||
)
|
)
|
||||||
|
|
||||||
const canManageOrganization = computed(() =>
|
const canManageOrganization = computed(() =>
|
||||||
hasPermission({ organization: ["manage_settings"] })
|
hasPermission({ organization: ["update"] })
|
||||||
)
|
)
|
||||||
|
|
||||||
// Role checks
|
// Role checks
|
||||||
@@ -42,6 +42,7 @@ export function usePermissions() {
|
|||||||
const isEmployee = computed(() => currentRole.value === ROLES.EMPLOYEE)
|
const isEmployee = computed(() => currentRole.value === ROLES.EMPLOYEE)
|
||||||
const isWorksCouncilMember = computed(() => currentRole.value === ROLES.WORKS_COUNCIL_MEMBER)
|
const isWorksCouncilMember = computed(() => currentRole.value === ROLES.WORKS_COUNCIL_MEMBER)
|
||||||
const isAdmin = computed(() => currentRole.value === ROLES.ADMIN)
|
const isAdmin = computed(() => currentRole.value === ROLES.ADMIN)
|
||||||
|
const isOwner = computed(() => currentRole.value === ROLES.OWNER)
|
||||||
|
|
||||||
const getCurrentRoleInfo = () => {
|
const getCurrentRoleInfo = () => {
|
||||||
const roleInfo = {
|
const roleInfo = {
|
||||||
@@ -68,6 +69,12 @@ export function usePermissions() {
|
|||||||
description: 'Vollzugriff auf Organisationsverwaltung',
|
description: 'Vollzugriff auf Organisationsverwaltung',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: 'i-lucide-settings'
|
icon: 'i-lucide-settings'
|
||||||
|
},
|
||||||
|
[ROLES.OWNER]: {
|
||||||
|
name: 'Eigentümer',
|
||||||
|
description: 'Vollzugriff und Organisationsbesitz',
|
||||||
|
color: 'yellow',
|
||||||
|
icon: 'i-lucide-crown'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +94,7 @@ export function usePermissions() {
|
|||||||
isEmployee,
|
isEmployee,
|
||||||
isWorksCouncilMember,
|
isWorksCouncilMember,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
isOwner,
|
||||||
|
|
||||||
// Computed permissions
|
// Computed permissions
|
||||||
canCreateApplicationForm,
|
canCreateApplicationForm,
|
||||||
|
|||||||
58
legalconsenthub/composables/user/useUser.ts
Normal file
58
legalconsenthub/composables/user/useUser.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
type CreateUserDto,
|
||||||
|
type UserDto,
|
||||||
|
ResponseError
|
||||||
|
} from '~/.api-client'
|
||||||
|
import { useUserApi } from './useUserApi'
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const userApi = useUserApi()
|
||||||
|
|
||||||
|
async function createUser(createUserDto: CreateUserDto): Promise<UserDto> {
|
||||||
|
try {
|
||||||
|
return await userApi.createUser(createUserDto)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof ResponseError) {
|
||||||
|
console.error('Failed creating user:', e.response)
|
||||||
|
} else {
|
||||||
|
console.error('Failed creating user:', e)
|
||||||
|
}
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserById(id: string): Promise<UserDto | null> {
|
||||||
|
try {
|
||||||
|
return await userApi.getUserById(id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof ResponseError && e.response.status === 404) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (e instanceof ResponseError) {
|
||||||
|
console.error(`Failed retrieving user with ID ${id}:`, e.response)
|
||||||
|
} else {
|
||||||
|
console.error(`Failed retrieving user with ID ${id}:`, e)
|
||||||
|
}
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(id: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
return await userApi.deleteUser(id)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof ResponseError) {
|
||||||
|
console.error(`Failed deleting user with ID ${id}:`, e.response)
|
||||||
|
} else {
|
||||||
|
console.error(`Failed deleting user with ID ${id}:`, e)
|
||||||
|
}
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createUser,
|
||||||
|
getUserById,
|
||||||
|
deleteUser
|
||||||
|
}
|
||||||
|
}
|
||||||
39
legalconsenthub/composables/user/useUserApi.ts
Normal file
39
legalconsenthub/composables/user/useUserApi.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
UserApi,
|
||||||
|
Configuration,
|
||||||
|
type CreateUserDto,
|
||||||
|
type UserDto
|
||||||
|
} from '~/.api-client'
|
||||||
|
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
|
||||||
|
|
||||||
|
export function useUserApi() {
|
||||||
|
const appBaseUrl = useRuntimeConfig().app.baseURL
|
||||||
|
const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public
|
||||||
|
const { jwt } = useAuth()
|
||||||
|
|
||||||
|
const basePath = withoutTrailingSlash(
|
||||||
|
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
|
||||||
|
)
|
||||||
|
|
||||||
|
const userApiClient = new UserApi(
|
||||||
|
new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })
|
||||||
|
)
|
||||||
|
|
||||||
|
async function createUser(createUserDto: CreateUserDto): Promise<UserDto> {
|
||||||
|
return userApiClient.createUser({ createUserDto })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserById(id: string): Promise<UserDto> {
|
||||||
|
return userApiClient.getUserById({ id })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(id: string): Promise<void> {
|
||||||
|
return userApiClient.deleteUser({ id })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createUser,
|
||||||
|
getUserById,
|
||||||
|
deleteUser
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,10 +29,25 @@
|
|||||||
</UDashboardSidebar>
|
</UDashboardSidebar>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
|
<NotificationsSlideover v-model="isNotificationsSlideoverOpen" />
|
||||||
</UDashboardGroup>
|
</UDashboardGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const links = [[], []]
|
const links = [[], []]
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
|
||||||
|
const isNotificationsSlideoverOpen = ref(false)
|
||||||
|
const { unreadCount, fetchUnreadCount, startPeriodicRefresh } = useNotification()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchUnreadCount()
|
||||||
|
startPeriodicRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('notificationState', {
|
||||||
|
isNotificationsSlideoverOpen,
|
||||||
|
unreadCount
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,13 +15,15 @@
|
|||||||
"api:generate": "openapi-generator-cli generate -i ../legalconsenthub-backend/api/legalconsenthub.yml -g typescript-fetch -o .api-client",
|
"api:generate": "openapi-generator-cli generate -i ../legalconsenthub-backend/api/legalconsenthub.yml -g typescript-fetch -o .api-client",
|
||||||
"api:middleware:generate": "openapi-generator-cli generate -i ../legalconsenthub-middleware/api/legalconsenthub-middleware.yml -g typescript-fetch -o .api-client-middleware",
|
"api:middleware:generate": "openapi-generator-cli generate -i ../legalconsenthub-middleware/api/legalconsenthub-middleware.yml -g typescript-fetch -o .api-client-middleware",
|
||||||
"fix:bettersqlite": "cd node_modules/better-sqlite3 && pnpm dlx node-gyp rebuild && cd ../..",
|
"fix:bettersqlite": "cd node_modules/better-sqlite3 && pnpm dlx node-gyp rebuild && cd ../..",
|
||||||
"generate:betterauth": "pnpm dlx @better-auth/cli generate --config server/utils/auth.ts",
|
"generate:betterauth": "pnpm dlx @better-auth/cli generate --config server/utils/auth.ts --yes",
|
||||||
"migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts"
|
"migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts --yes",
|
||||||
|
"recreate-db:betterauth": "rm sqlite.db && pnpm run migrate:betterauth && pnpm run migrate:betterauth"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui-pro": "3.1.1",
|
"@nuxt/ui-pro": "3.1.1",
|
||||||
"@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",
|
||||||
"better-auth": "1.3.4",
|
"better-auth": "1.3.4",
|
||||||
"better-sqlite3": "11.8.1",
|
"better-sqlite3": "11.8.1",
|
||||||
"nuxt": "3.16.1",
|
"nuxt": "3.16.1",
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
}"
|
}"
|
||||||
class="w-48"
|
class="w-48"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UTooltip text="Notifications" :shortcuts="['N']">
|
||||||
|
<UButton color="neutral" variant="ghost" square @click="isNotificationsSlideoverOpen = true">
|
||||||
|
<UChip :show="unreadCount > 0" color="error" inset>
|
||||||
|
<UIcon name="i-lucide-bell" class="size-5 shrink-0" />
|
||||||
|
<span v-if="unreadCount > 0" class="ml-1 text-xs">{{ unreadCount }}</span>
|
||||||
|
</UChip>
|
||||||
|
</UButton>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
<UDropdownMenu :items="items">
|
<UDropdownMenu :items="items">
|
||||||
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
|
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
@@ -49,11 +59,7 @@
|
|||||||
<p class="text-(--ui-text-muted) text-sm">
|
<p class="text-(--ui-text-muted) text-sm">
|
||||||
Erstellt von {{ applicationFormElem.createdBy.name }} am {{ formatDate(applicationFormElem.createdAt) }}
|
Erstellt von {{ applicationFormElem.createdBy.name }} am {{ formatDate(applicationFormElem.createdAt) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2">
|
<p class="text-(--ui-text-muted) text-sm">Status: {{ applicationFormElem.status }}</p>
|
||||||
<UChip size="sm">
|
|
||||||
{{ applicationFormElem.status }}
|
|
||||||
</UChip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<UPageLinks :links="getLinksForApplicationForm(applicationFormElem)" />
|
<UPageLinks :links="getLinksForApplicationForm(applicationFormElem)" />
|
||||||
@@ -77,6 +83,12 @@ const { getAllApplicationForms, deleteApplicationFormById } = useApplicationForm
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { organizations, selectedOrganization } = useAuth()
|
const { organizations, selectedOrganization } = useAuth()
|
||||||
|
|
||||||
|
// Inject notification state from layout
|
||||||
|
const { isNotificationsSlideoverOpen, unreadCount } = inject('notificationState', {
|
||||||
|
isNotificationsSlideoverOpen: ref(false),
|
||||||
|
unreadCount: ref(0)
|
||||||
|
})
|
||||||
|
|
||||||
const { data } = await useAsyncData<PagedApplicationFormDto>(
|
const { data } = await useAsyncData<PagedApplicationFormDto>(
|
||||||
async () => {
|
async () => {
|
||||||
if (!selectedOrganization.value) {
|
if (!selectedOrganization.value) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||||
|
import { UserRole } from '~/.api-client'
|
||||||
|
|
||||||
definePageMeta({ layout: 'auth' })
|
definePageMeta({ layout: 'auth' })
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ useSeoMeta({ title: 'Sign up' })
|
|||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { signUp } = useAuth()
|
const { signUp } = useAuth()
|
||||||
|
const { createUser } = useUser()
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
{
|
{
|
||||||
@@ -90,8 +92,28 @@ function onSubmit(payload: FormSubmitEvent<Schema>) {
|
|||||||
// TODO: Hide loading spinner
|
// TODO: Hide loading spinner
|
||||||
console.log('Receiving register response')
|
console.log('Receiving register response')
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async (ctx) => {
|
||||||
console.log('Successfully registered!')
|
console.log('Successfully registered!')
|
||||||
|
|
||||||
|
// Create user in backend after successful Better Auth registration
|
||||||
|
try {
|
||||||
|
console.log('Creating user in backend...', ctx.data)
|
||||||
|
await createUser({
|
||||||
|
id: ctx.data.user.id,
|
||||||
|
name: ctx.data.user.name,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
role: UserRole.Employee
|
||||||
|
})
|
||||||
|
console.log('User created in backend successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create user in backend:', error)
|
||||||
|
toast.add({
|
||||||
|
title: 'Warning',
|
||||||
|
description: 'Account created but there was an issue with backend setup. Please contact support.',
|
||||||
|
color: 'warning'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
await navigateTo('/')
|
await navigateTo('/')
|
||||||
},
|
},
|
||||||
onError: (ctx) => {
|
onError: (ctx) => {
|
||||||
|
|||||||
41
legalconsenthub/pnpm-lock.yaml
generated
41
legalconsenthub/pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
'@pinia/nuxt':
|
'@pinia/nuxt':
|
||||||
specifier: 0.10.1
|
specifier: 0.10.1
|
||||||
version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))
|
version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))
|
||||||
|
'@vueuse/core':
|
||||||
|
specifier: ^13.6.0
|
||||||
|
version: 13.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.3.4
|
specifier: 1.3.4
|
||||||
version: 1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -2143,6 +2146,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@vueuse/core@13.6.0':
|
||||||
|
resolution: {integrity: sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
'@vueuse/integrations@13.1.0':
|
'@vueuse/integrations@13.1.0':
|
||||||
resolution: {integrity: sha512-wJ6aANdUs4SOpVabChQK+uLIwxRTUAEmn1DJnflGG7Wq6yaipiRmp6as/Md201FjJnquQt8MecIPbFv8HSBeDA==}
|
resolution: {integrity: sha512-wJ6aANdUs4SOpVabChQK+uLIwxRTUAEmn1DJnflGG7Wq6yaipiRmp6as/Md201FjJnquQt8MecIPbFv8HSBeDA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2194,6 +2202,9 @@ packages:
|
|||||||
'@vueuse/metadata@13.1.0':
|
'@vueuse/metadata@13.1.0':
|
||||||
resolution: {integrity: sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==}
|
resolution: {integrity: sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==}
|
||||||
|
|
||||||
|
'@vueuse/metadata@13.6.0':
|
||||||
|
resolution: {integrity: sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==}
|
||||||
|
|
||||||
'@vueuse/shared@10.11.1':
|
'@vueuse/shared@10.11.1':
|
||||||
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
||||||
|
|
||||||
@@ -2205,6 +2216,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
'@vueuse/shared@13.6.0':
|
||||||
|
resolution: {integrity: sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.5.0
|
||||||
|
|
||||||
abbrev@3.0.0:
|
abbrev@3.0.0:
|
||||||
resolution: {integrity: sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==}
|
resolution: {integrity: sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -6974,7 +6990,7 @@ snapshots:
|
|||||||
'@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.0.10)
|
||||||
'@standard-schema/spec': 1.0.0
|
'@standard-schema/spec': 1.0.0
|
||||||
'@vueuse/core': 13.1.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
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
dotenv: 16.5.0
|
dotenv: 16.5.0
|
||||||
@@ -6987,7 +7003,7 @@ snapshots:
|
|||||||
tinyglobby: 0.2.13
|
tinyglobby: 0.2.13
|
||||||
typescript: 5.7.3
|
typescript: 5.7.3
|
||||||
unplugin: 2.3.2
|
unplugin: 2.3.2
|
||||||
unplugin-auto-import: 19.1.2(@nuxt/kit@3.17.2(magicast@0.3.5))(@vueuse/core@13.1.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.0.10
|
||||||
@@ -7046,7 +7062,7 @@ snapshots:
|
|||||||
'@tailwindcss/vite': 4.1.6(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))
|
'@tailwindcss/vite': 4.1.6(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))
|
||||||
'@tanstack/vue-table': 8.21.3(vue@3.5.13(typescript@5.7.3))
|
'@tanstack/vue-table': 8.21.3(vue@3.5.13(typescript@5.7.3))
|
||||||
'@unhead/vue': 2.0.8(vue@3.5.13(typescript@5.7.3))
|
'@unhead/vue': 2.0.8(vue@3.5.13(typescript@5.7.3))
|
||||||
'@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.7.3))
|
'@vueuse/core': 13.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
'@vueuse/integrations': 13.1.0(axios@1.7.9)(fuse.js@7.1.0)(vue@3.5.13(typescript@5.7.3))
|
'@vueuse/integrations': 13.1.0(axios@1.7.9)(fuse.js@7.1.0)(vue@3.5.13(typescript@5.7.3))
|
||||||
colortranslator: 4.1.0
|
colortranslator: 4.1.0
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
@@ -7072,7 +7088,7 @@ snapshots:
|
|||||||
tinyglobby: 0.2.13
|
tinyglobby: 0.2.13
|
||||||
typescript: 5.7.3
|
typescript: 5.7.3
|
||||||
unplugin: 2.3.2
|
unplugin: 2.3.2
|
||||||
unplugin-auto-import: 19.1.2(@nuxt/kit@3.17.2(magicast@0.3.5))(@vueuse/core@13.1.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))
|
||||||
vaul-vue: 0.4.1(reka-ui@2.2.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
vaul-vue: 0.4.1(reka-ui@2.2.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
|
||||||
vue-component-type-helpers: 2.2.10
|
vue-component-type-helpers: 2.2.10
|
||||||
@@ -8201,6 +8217,13 @@ snapshots:
|
|||||||
'@vueuse/shared': 13.1.0(vue@3.5.13(typescript@5.7.3))
|
'@vueuse/shared': 13.1.0(vue@3.5.13(typescript@5.7.3))
|
||||||
vue: 3.5.13(typescript@5.7.3)
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
|
'@vueuse/core@13.6.0(vue@3.5.13(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
'@types/web-bluetooth': 0.0.21
|
||||||
|
'@vueuse/metadata': 13.6.0
|
||||||
|
'@vueuse/shared': 13.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
'@vueuse/integrations@13.1.0(axios@1.7.9)(fuse.js@7.1.0)(vue@3.5.13(typescript@5.7.3))':
|
'@vueuse/integrations@13.1.0(axios@1.7.9)(fuse.js@7.1.0)(vue@3.5.13(typescript@5.7.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.7.3))
|
'@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.7.3))
|
||||||
@@ -8216,6 +8239,8 @@ snapshots:
|
|||||||
|
|
||||||
'@vueuse/metadata@13.1.0': {}
|
'@vueuse/metadata@13.1.0': {}
|
||||||
|
|
||||||
|
'@vueuse/metadata@13.6.0': {}
|
||||||
|
|
||||||
'@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.7.3))':
|
'@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.7.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
|
vue-demi: 0.14.10(vue@3.5.13(typescript@5.7.3))
|
||||||
@@ -8233,6 +8258,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.7.3)
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
|
'@vueuse/shared@13.6.0(vue@3.5.13(typescript@5.7.3))':
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
abbrev@3.0.0: {}
|
abbrev@3.0.0: {}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
@@ -11626,7 +11655,7 @@ snapshots:
|
|||||||
|
|
||||||
universalify@2.0.1: {}
|
universalify@2.0.1: {}
|
||||||
|
|
||||||
unplugin-auto-import@19.1.2(@nuxt/kit@3.17.2(magicast@0.3.5))(@vueuse/core@13.1.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))):
|
||||||
dependencies:
|
dependencies:
|
||||||
local-pkg: 1.1.1
|
local-pkg: 1.1.1
|
||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
@@ -11636,7 +11665,7 @@ snapshots:
|
|||||||
unplugin-utils: 0.2.4
|
unplugin-utils: 0.2.4
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@nuxt/kit': 3.17.2(magicast@0.3.5)
|
'@nuxt/kit': 3.17.2(magicast@0.3.5)
|
||||||
'@vueuse/core': 13.1.0(vue@3.5.13(typescript@5.7.3))
|
'@vueuse/core': 13.6.0(vue@3.5.13(typescript@5.7.3))
|
||||||
|
|
||||||
unplugin-utils@0.2.4:
|
unplugin-utils@0.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ export const auth = betterAuth({
|
|||||||
|
|
||||||
const roleDisplayName = roleDisplayNames[data.role as LegalRole] || data.role
|
const roleDisplayName = roleDisplayNames[data.role as LegalRole] || data.role
|
||||||
|
|
||||||
await resend.emails.send({
|
try {
|
||||||
from: 'Legal Consent Hub <noreply@legalconsenthub.com>',
|
const result = await resend.emails.send({
|
||||||
|
from: 'Acme <onboarding@resend.dev>',
|
||||||
to: data.email,
|
to: data.email,
|
||||||
subject: `Einladung als ${roleDisplayName} - ${data.organization.name}`,
|
subject: `Einladung als ${roleDisplayName} - ${data.organization.name}`,
|
||||||
html: `
|
html: `
|
||||||
@@ -66,6 +67,27 @@ export const auth = betterAuth({
|
|||||||
<p>Diese Einladung läuft ab am: ${new Date(data.invitation.expiresAt).toLocaleDateString('de-DE')}</p>
|
<p>Diese Einladung läuft ab am: ${new Date(data.invitation.expiresAt).toLocaleDateString('de-DE')}</p>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(`Email sending failed: ${result.error?.statusCode} ${result.error?.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Email invite link:', inviteLink)
|
||||||
|
console.log('Invitation email sent successfully to:', data.email, 'with ID:', result.data?.id)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send invitation email:', error)
|
||||||
|
|
||||||
|
// Log specific error details for debugging
|
||||||
|
const errorObj = error as { response?: { status: number; data: unknown }; message?: string }
|
||||||
|
if (errorObj.response) {
|
||||||
|
console.error('HTTP Status:', errorObj.response.status)
|
||||||
|
console.error('Response data:', errorObj.response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-throw the error so BetterAuth knows the email failed
|
||||||
|
const message = errorObj.message || String(error)
|
||||||
|
throw new Error(`Email sending failed: ${message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,55 +1,87 @@
|
|||||||
import { createAccessControl } from 'better-auth/plugins/access'
|
import { createAccessControl } from 'better-auth/plugins/access'
|
||||||
|
import { defaultStatements, adminAc, memberAc, ownerAc } from 'better-auth/plugins/organization/access'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
|
||||||
export const statement = {
|
const customStatements = {
|
||||||
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject', 'submit'],
|
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject', 'submit'],
|
||||||
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
||||||
organization: ['create', 'read', 'update', 'delete', 'manage_settings'],
|
|
||||||
member: ['create', 'read', 'update', 'delete', 'invite', 'remove'],
|
|
||||||
comment: ['create', 'read', 'update', 'delete'],
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const statement = {
|
||||||
|
...customStatements,
|
||||||
|
...defaultStatements
|
||||||
|
} as const
|
||||||
|
|
||||||
export const accessControl = createAccessControl(statement)
|
export const accessControl = createAccessControl(statement)
|
||||||
|
|
||||||
// Roles with specific permissions
|
export const employerRole = accessControl.newRole(
|
||||||
export const employerRole = accessControl.newRole({
|
defu(
|
||||||
|
{
|
||||||
application_form: ['create', 'read', 'approve', 'reject'],
|
application_form: ['create', 'read', 'approve', 'reject'],
|
||||||
agreement: ['create', 'read', 'sign', 'approve'],
|
agreement: ['create', 'read', 'sign', 'approve'],
|
||||||
member: ['invite', 'read'],
|
|
||||||
comment: ['create', 'read', 'update', 'delete'],
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
})
|
},
|
||||||
|
memberAc.statements
|
||||||
|
) as Parameters<typeof accessControl.newRole>[0]
|
||||||
|
)
|
||||||
|
|
||||||
export const worksCouncilMemberRole = accessControl.newRole({
|
export const worksCouncilMemberRole = accessControl.newRole(
|
||||||
|
defu(
|
||||||
|
{
|
||||||
application_form: ['create', 'read', 'update', 'submit'],
|
application_form: ['create', 'read', 'update', 'submit'],
|
||||||
agreement: ['read', 'sign', 'approve'],
|
agreement: ['read', 'sign', 'approve'],
|
||||||
member: ['read'],
|
|
||||||
comment: ['create', 'read', 'update', 'delete'],
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
document: ['create', 'read', 'update', 'download', 'upload']
|
document: ['create', 'read', 'update', 'download', 'upload']
|
||||||
})
|
},
|
||||||
|
memberAc.statements
|
||||||
|
) as Parameters<typeof accessControl.newRole>[0]
|
||||||
|
)
|
||||||
|
|
||||||
export const employeeRole = accessControl.newRole({
|
export const employeeRole = accessControl.newRole(
|
||||||
|
defu(
|
||||||
|
{
|
||||||
application_form: ['read'],
|
application_form: ['read'],
|
||||||
agreement: ['read'],
|
agreement: ['read'],
|
||||||
member: ['read'],
|
|
||||||
comment: ['create', 'read'],
|
comment: ['create', 'read'],
|
||||||
document: ['read', 'download']
|
document: ['read', 'download']
|
||||||
})
|
},
|
||||||
|
memberAc.statements
|
||||||
|
) as Parameters<typeof accessControl.newRole>[0]
|
||||||
|
)
|
||||||
|
|
||||||
export const adminRole = accessControl.newRole({
|
export const adminRole = accessControl.newRole(
|
||||||
|
defu(
|
||||||
|
{
|
||||||
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject'],
|
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject'],
|
||||||
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
||||||
organization: ['create', 'read', 'update', 'delete', 'manage_settings'],
|
|
||||||
member: ['create', 'read', 'update', 'delete', 'invite', 'remove'],
|
|
||||||
comment: ['create', 'read', 'update', 'delete'],
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
})
|
},
|
||||||
|
adminAc.statements
|
||||||
|
) as Parameters<typeof accessControl.newRole>[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ownerRole = accessControl.newRole(
|
||||||
|
defu(
|
||||||
|
{
|
||||||
|
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject', 'submit'],
|
||||||
|
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
|
||||||
|
comment: ['create', 'read', 'update', 'delete'],
|
||||||
|
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
|
||||||
|
},
|
||||||
|
ownerAc.statements
|
||||||
|
) as Parameters<typeof accessControl.newRole>[0]
|
||||||
|
)
|
||||||
|
|
||||||
export const ROLES = {
|
export const ROLES = {
|
||||||
EMPLOYER: 'employer',
|
EMPLOYER: 'employer',
|
||||||
WORKS_COUNCIL_MEMBER: 'works_council_member',
|
WORKS_COUNCIL_MEMBER: 'works_council_member',
|
||||||
EMPLOYEE: 'employee',
|
EMPLOYEE: 'employee',
|
||||||
ADMIN: 'admin'
|
ADMIN: 'admin',
|
||||||
|
OWNER: 'owner'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type LegalRole = (typeof ROLES)[keyof typeof ROLES]
|
export type LegalRole = (typeof ROLES)[keyof typeof ROLES]
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"isTemplate": true,
|
"isTemplate": true,
|
||||||
"name": "",
|
"name": "",
|
||||||
"createdBy": "Denis",
|
|
||||||
"lastModifiedBy": "Denis",
|
|
||||||
"formElementSections": [
|
"formElementSections": [
|
||||||
{
|
{
|
||||||
"title": "Section 1",
|
"title": "Section 1",
|
||||||
|
|||||||
Reference in New Issue
Block a user