diff --git a/legalconsenthub-backend/api/legalconsenthub.yml b/legalconsenthub-backend/api/legalconsenthub.yml index 5388762..0b30a2e 100644 --- a/legalconsenthub-backend/api/legalconsenthub.yml +++ b/legalconsenthub-backend/api/legalconsenthub.yml @@ -339,34 +339,6 @@ paths: $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" ####### 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}: parameters: - name: id @@ -394,35 +366,6 @@ paths: $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" - put: - summary: Update a user - operationId: updateUser - description: Updates a user. If no request body is provided, the user data will be synchronized from JWT token claims. - tags: - - user - requestBody: - required: false - content: - application/json: - schema: - $ref: "#/components/schemas/UserDto" - responses: - "200": - description: User successfully updated - content: - application/json: - schema: - $ref: "#/components/schemas/UserDto" - "400": - $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest" - "401": - $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized" - "404": - $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/NotFound" - "500": - $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError" - "503": - $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" delete: summary: Delete a user operationId: deleteUser @@ -1075,45 +1018,17 @@ components: UserDto: type: object required: - - id + - keycloakId - name - - status - - organizationRoles + - organizationId properties: - id: + keycloakId: type: string name: type: string - status: - $ref: "#/components/schemas/UserStatus" - organizationRoles: - type: object - additionalProperties: - type: array - items: - $ref: "#/components/schemas/UserRole" - description: "Map of organization IDs to arrays of user roles in those organizations" - - CreateUserDto: - type: object - required: - - id - - name - - status - properties: - id: + organizationId: type: string - name: - type: string - status: - $ref: "#/components/schemas/UserStatus" - organizationRoles: - type: object - additionalProperties: - type: array - items: - $ref: "#/components/schemas/UserRole" - description: "Map of organization IDs to arrays of user roles in those organizations" + nullable: true UserStatus: type: string diff --git a/legalconsenthub-backend/build.gradle b/legalconsenthub-backend/build.gradle index 835ecb0..f510d55 100644 --- a/legalconsenthub-backend/build.gradle +++ b/legalconsenthub-backend/build.gradle @@ -49,13 +49,11 @@ dependencies { implementation "com.openhtmltopdf:openhtmltopdf-slf4j:$openHtmlVersion" implementation "com.openhtmltopdf:openhtmltopdf-svg-support:$openHtmlVersion" runtimeOnly 'com.h2database:h2' - runtimeOnly 'org.postgresql:postgresql' + implementation 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-testcontainers' implementation 'org.testcontainers:postgresql' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' 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' } diff --git a/legalconsenthub-backend/docker-compose.yaml b/legalconsenthub-backend/docker-compose.yaml new file mode 100644 index 0000000..4ac021f --- /dev/null +++ b/legalconsenthub-backend/docker-compose.yaml @@ -0,0 +1,45 @@ +networks: + net: + driver: bridge + +volumes: + postgres_data_testbed: + +services: + db: + image: postgres:latest + container_name: postgres-testbed + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + env_file: + - .env + ports: + - 5532:5432 + networks: + - net + volumes: + - postgres_data_testbed:/var/lib/postgresql/data + + keycloak: + image: quay.io/keycloak/keycloak:26.4.0 + container_name: keycloak-testbed + command: start-dev + environment: + KC_DB: postgres + KC_DB_URL_HOST: db + KC_DB_USERNAME: ${POSTGRES_USER} + KC_DB_PASSWORD: ${POSTGRES_PASSWORD} + KC_DB_DATABASE: ${POSTGRES_DB} + KC_DB_SCHEMA: public + KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + env_file: + - .env + ports: + - 7080:8080 + depends_on: + - db + networks: + - net diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt index e970b84..ee903b8 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/application_form/ApplicationFormService.kt @@ -5,7 +5,7 @@ import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedExc import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotFoundException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotUpdatedException -import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService +// import com.betriebsratkanzlei.legalconsenthub.notification.NotificationService import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.ApplicationFormStatus import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto @@ -19,7 +19,7 @@ import java.util.UUID class ApplicationFormService( private val applicationFormRepository: ApplicationFormRepository, private val applicationFormMapper: ApplicationFormMapper, - private val notificationService: NotificationService + // private val notificationService: NotificationService ) { fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { @@ -96,19 +96,19 @@ class ApplicationFormService( 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 separate notification for each role that should be notified - val rolesToNotify = listOf("admin", "works_council_member", "employer", "employee") - - rolesToNotify.forEach { role -> - notificationService.createNotification( - title = title, - message = message, - clickTarget = clickTarget, - recipient = null, - role = role, - organizationId = applicationForm.organizationId, - type = NotificationType.INFO - ) - } + // // Create separate notification for each role that should be notified + // val rolesToNotify = listOf("admin", "works_council_member", "employer", "employee") + // + // rolesToNotify.forEach { role -> + // notificationService.createNotification( + // title = title, + // message = message, + // clickTarget = clickTarget, + // recipient = null, + // role = role, + // organizationId = applicationForm.organizationId, + // type = NotificationType.INFO + // ) + // } } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt index 00273b3..9d46ea7 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/config/SecurityConfig.kt @@ -18,32 +18,16 @@ import org.springframework.http.HttpMethod class SecurityConfig { @Bean - @Order(1) - fun publicApiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { - http { - securityMatcher("/swagger-ui/**", "/v3/**", "/actuator/**") - csrf { disable() } - authorizeHttpRequests { - authorize("/swagger-ui/**", permitAll) - authorize("/v3/**", permitAll) - authorize("/actuator/**", permitAll) - authorize(anyRequest, denyAll) - } - } - return http.build() - } - - @Bean - @Order(2) - fun protectedApiSecurityFilterChain( + fun configure( http: HttpSecurity, customJwtAuthenticationConverter: CustomJwtAuthenticationConverter ): SecurityFilterChain { http { csrf { disable() } authorizeHttpRequests { - // Allow user registration without authentication - authorize(HttpMethod.POST, "/users", permitAll) + authorize("/swagger-ui/**", permitAll) + authorize("/v3/**", permitAll) + authorize("/actuator/**", permitAll) authorize(anyRequest, authenticated) } oauth2ResourceServer { @@ -52,10 +36,4 @@ class SecurityConfig { } return http.build() } - - @Bean - fun jwtDecoder(): JwtDecoder { - return NimbusJwtDecoder.withJwkSetUri("http://192.168.178.114:3001/api/auth/jwks") - .jwsAlgorithm(SignatureAlgorithm.ES512).build() - } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt index e6b2ec9..a3b43d8 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationController.kt @@ -1,83 +1,83 @@ -package com.betriebsratkanzlei.legalconsenthub.notification - -import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal -import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi -import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto -import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto -import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto -import org.springframework.http.ResponseEntity -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.web.bind.annotation.RestController -import java.util.UUID - -@RestController -class NotificationController( - private val notificationService: NotificationService, - private val notificationMapper: NotificationMapper, - private val pagedNotificationMapper: PagedNotificationMapper -) : NotificationApi { - - override fun getNotifications(page: Int, size: Int): ResponseEntity { - val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal - val recipientId = principal.id ?: throw IllegalStateException("User ID not found") - val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") - - val notifications = notificationService.getNotifications( - recipientId = recipientId, - organizationId = organizationId, - page = page, - size = size - ) - - return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications)) - } - - override fun getUnreadNotifications(): ResponseEntity> { - val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal - val recipientId = principal.id ?: throw IllegalStateException("User ID not found") - val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") - - val notifications = notificationService.getUnreadNotifications( - recipientId = recipientId, - organizationId = organizationId - ) - - return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) }) - } - - override fun getUnreadNotificationCount(): ResponseEntity { - val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal - val recipientId = principal.id ?: throw IllegalStateException("User ID not found") - val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") - - val count = notificationService.getUnreadNotificationCount( - recipientId = recipientId, - organizationId = organizationId - ) - - return ResponseEntity.ok(count) - } - - override fun markAllNotificationsAsRead(): ResponseEntity { - val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal - val recipientId = principal.id ?: throw IllegalStateException("User ID not found") - val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") - - notificationService.markAllAsRead( - recipientId = recipientId, - organizationId = organizationId - ) - - return ResponseEntity.noContent().build() - } - - override fun markNotificationAsRead(id: UUID): ResponseEntity { - val notification = notificationService.markNotificationAsRead(id) - return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) - } - - override fun createNotification(createNotificationDto: CreateNotificationDto): ResponseEntity { - val notification = notificationService.createNotification(createNotificationDto) - return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) - } -} +// package com.betriebsratkanzlei.legalconsenthub.notification +// +// import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal +// import com.betriebsratkanzlei.legalconsenthub_api.api.NotificationApi +// import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto +// import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationDto +// import com.betriebsratkanzlei.legalconsenthub_api.model.PagedNotificationDto +// import org.springframework.http.ResponseEntity +// import org.springframework.security.core.context.SecurityContextHolder +// import org.springframework.web.bind.annotation.RestController +// import java.util.UUID +// +// @RestController +// class NotificationController( +// private val notificationService: NotificationService, +// private val notificationMapper: NotificationMapper, +// private val pagedNotificationMapper: PagedNotificationMapper +// ) : NotificationApi { +// +// override fun getNotifications(page: Int, size: Int): ResponseEntity { +// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal +// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") +// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") +// +// val notifications = notificationService.getNotifications( +// recipientId = recipientId, +// organizationId = organizationId, +// page = page, +// size = size +// ) +// +// return ResponseEntity.ok(pagedNotificationMapper.toPagedNotificationDto(notifications)) +// } +// +// override fun getUnreadNotifications(): ResponseEntity> { +// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal +// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") +// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") +// +// val notifications = notificationService.getUnreadNotifications( +// recipientId = recipientId, +// organizationId = organizationId +// ) +// +// return ResponseEntity.ok(notifications.map { notificationMapper.toNotificationDto(it) }) +// } +// +// override fun getUnreadNotificationCount(): ResponseEntity { +// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal +// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") +// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") +// +// val count = notificationService.getUnreadNotificationCount( +// recipientId = recipientId, +// organizationId = organizationId +// ) +// +// return ResponseEntity.ok(count) +// } +// +// override fun markAllNotificationsAsRead(): ResponseEntity { +// val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal +// val recipientId = principal.id ?: throw IllegalStateException("User ID not found") +// val organizationId = principal.organizationId ?: throw IllegalStateException("Organization ID not found") +// +// notificationService.markAllAsRead( +// recipientId = recipientId, +// organizationId = organizationId +// ) +// +// return ResponseEntity.noContent().build() +// } +// +// override fun markNotificationAsRead(id: UUID): ResponseEntity { +// val notification = notificationService.markNotificationAsRead(id) +// return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) +// } +// +// override fun createNotification(createNotificationDto: CreateNotificationDto): ResponseEntity { +// val notification = notificationService.createNotification(createNotificationDto) +// return ResponseEntity.ok(notificationMapper.toNotificationDto(notification)) +// } +// } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt index 667dce1..eb13f57 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationRepository.kt @@ -1,76 +1,84 @@ -package com.betriebsratkanzlei.legalconsenthub.notification - -import org.springframework.data.domain.Page -import org.springframework.data.domain.Pageable -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Modifying -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.stereotype.Repository -import java.util.UUID - -@Repository -interface NotificationRepository : JpaRepository { - - fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page - fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List - fun countByRecipientIdAndIsReadFalse(recipientId: String?): Long - - @Query(""" - SELECT n FROM Notification n WHERE - (n.recipient.id = :recipientId) OR - (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR - (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) - ORDER BY n.createdAt DESC - """) - fun findUserNotificationsByOrgRole( - @Param("recipientId") recipientId: String, - @Param("organizationIds") organizationIds: List, - @Param("orgRolePairs") orgRolePairs: List, - pageable: Pageable - ): Page - - @Query(""" - SELECT n FROM Notification n WHERE - ((n.recipient.id = :recipientId) OR - (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR - (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) - AND n.isRead = false - ORDER BY n.createdAt DESC - """) - fun findUnreadUserNotificationsByOrgRole( - @Param("recipientId") recipientId: String, - @Param("organizationIds") organizationIds: List, - @Param("orgRolePairs") orgRolePairs: List - ): List - - @Query(""" - SELECT COUNT(n) FROM Notification n WHERE - ((n.recipient.id = :recipientId) OR - (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR - (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) - AND n.isRead = false - """) - fun countUnreadUserNotifications( - @Param("recipientId") recipientId: String, - @Param("organizationIds") organizationIds: List, - @Param("orgRolePairs") orgRolePairs: List - ): Long - - @Modifying - @Query(""" - UPDATE Notification n SET n.isRead = true WHERE - (n.recipient.id = :recipientId) OR - (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR - (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) - """) - fun markAllUserNotificationsAsRead( - @Param("recipientId") recipientId: String, - @Param("organizationIds") organizationIds: List, - @Param("orgRolePairs") orgRolePairs: List - ) - - @Modifying - @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.id = :recipientId") - fun markAllAsReadByRecipientId(@Param("recipientId") recipientId: String) -} +// package com.betriebsratkanzlei.legalconsenthub.notification +// +// import org.springframework.data.domain.Page +// import org.springframework.data.domain.Pageable +// import org.springframework.data.jpa.repository.JpaRepository +// import org.springframework.data.jpa.repository.Modifying +// import org.springframework.data.jpa.repository.Query +// import org.springframework.data.repository.query.Param +// import org.springframework.stereotype.Repository +// import java.util.UUID +// +// @Repository +// interface NotificationRepository : JpaRepository { +// +// fun findByRecipientIdOrderByCreatedAtDesc(recipientId: String?, pageable: Pageable): Page +// fun findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId: String?): List +// fun countByRecipientIdAndIsReadFalse(recipientId: String?): Long +// +// @Query( +// """ +// SELECT n FROM Notification n WHERE +// (n.recipient.keycloakId = :recipientId) OR +// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR +// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) +// ORDER BY n.createdAt DESC +// """ +// ) +// fun findUserNotificationsByOrgRole( +// @Param("recipientId") recipientId: String, +// @Param("organizationIds") organizationIds: List, +// @Param("orgRolePairs") orgRolePairs: List, +// pageable: Pageable +// ): Page +// +// @Query( +// """ +// SELECT n FROM Notification n WHERE +// ((n.recipient.keycloakId = :recipientId) OR +// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR +// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) +// AND n.isRead = false +// ORDER BY n.createdAt DESC +// """ +// ) +// fun findUnreadUserNotificationsByOrgRole( +// @Param("recipientId") recipientId: String, +// @Param("organizationIds") organizationIds: List, +// @Param("orgRolePairs") orgRolePairs: List +// ): List +// +// @Query( +// """ +// SELECT COUNT(n) FROM Notification n WHERE +// ((n.recipient.keycloakId = :recipientId) OR +// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR +// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = ''))) +// AND n.isRead = false +// """ +// ) +// fun countUnreadUserNotifications( +// @Param("recipientId") recipientId: String, +// @Param("organizationIds") organizationIds: List, +// @Param("orgRolePairs") orgRolePairs: List +// ): Long +// +// @Modifying +// @Query( +// """ +// UPDATE Notification n SET n.isRead = true WHERE +// (n.recipient.keycloakId = :recipientId) OR +// (n.recipient IS NULL AND CONCAT(n.organizationId, ':', n.role) IN :orgRolePairs) OR +// (n.recipient IS NULL AND n.organizationId IN :organizationIds AND (n.role IS NULL OR n.role = '')) +// """ +// ) +// fun markAllUserNotificationsAsRead( +// @Param("recipientId") recipientId: String, +// @Param("organizationIds") organizationIds: List, +// @Param("orgRolePairs") orgRolePairs: List +// ) +// +// @Modifying +// @Query("UPDATE Notification n SET n.isRead = true WHERE n.recipient.keycloakId = :recipientId") +// fun markAllAsReadByRecipientId(@Param("recipientId") recipientId: String) +// } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt index c9d644f..2e0aeb0 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/notification/NotificationService.kt @@ -1,127 +1,127 @@ -package com.betriebsratkanzlei.legalconsenthub.notification - -import com.betriebsratkanzlei.legalconsenthub.user.User -import com.betriebsratkanzlei.legalconsenthub.user.UserRepository -import com.betriebsratkanzlei.legalconsenthub.user.UserRoleConverter -import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto -import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType -import org.springframework.data.domain.Page -import org.springframework.data.domain.PageRequest -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.util.UUID - -@Service -class NotificationService( - private val notificationRepository: NotificationRepository, - private val notificationMapper: NotificationMapper, - private val userRepository: UserRepository, - private val userRoleConverter: UserRoleConverter -) { - - fun createNotification(createNotificationDto: CreateNotificationDto): Notification { - val notification = notificationMapper.toNotification(createNotificationDto) - return notificationRepository.save(notification) - } - - fun createNotification( - title: String, - message: String, - clickTarget: String, - recipient: User?, - role: String, - organizationId: String, - type: NotificationType = NotificationType.INFO - ): Notification { - val notification = Notification( - title = title, - message = message, - clickTarget = clickTarget, - recipient = recipient, - role = role, - type = type, - organizationId = organizationId - ) - return notificationRepository.save(notification) - } - - fun getNotifications( - recipientId: String, - organizationId: String, - page: Int = 0, - size: Int = 20 - ): Page { - val user = userRepository.findById(recipientId) - .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } - - val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) - val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } - - val pageable = PageRequest.of(page, size) - return if (userRoles.isNotEmpty()) { - notificationRepository.findUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs, pageable) - } else { - notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, pageable) - } - } - - fun getUnreadNotifications( - recipientId: String, - organizationId: String - ): List { - val user = userRepository.findById(recipientId) - .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } - - val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) - val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } - - return if (userRoles.isNotEmpty()) { - notificationRepository.findUnreadUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs) - } else { - notificationRepository.findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId) - } - } - - fun getUnreadNotificationCount( - recipientId: String, - organizationId: String - ): Long { - val user = userRepository.findById(recipientId) - .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } - - val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) - val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } - - return if (userRoles.isNotEmpty()) { - notificationRepository.countUnreadUserNotifications(recipientId, listOf(organizationId), orgRolePairs) - } else { - notificationRepository.countByRecipientIdAndIsReadFalse(recipientId) - } - } - - @Transactional - fun markAllAsRead( - recipientId: String, - organizationId: String - ) { - val user = userRepository.findById(recipientId) - .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } - - val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) - val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } - - if (userRoles.isNotEmpty()) { - notificationRepository.markAllUserNotificationsAsRead(recipientId, listOf(organizationId), orgRolePairs) - } else { - notificationRepository.markAllAsReadByRecipientId(recipientId) - } - } - - @Transactional - fun markNotificationAsRead(notificationId: UUID): Notification { - val notification = notificationRepository.findById(notificationId) - .orElseThrow { IllegalArgumentException("Notification not found with id: $notificationId") } - notification.isRead = true - return notificationRepository.save(notification) - } -} +// package com.betriebsratkanzlei.legalconsenthub.notification +// +// import com.betriebsratkanzlei.legalconsenthub.user.User +// import com.betriebsratkanzlei.legalconsenthub.user.UserRepository +// import com.betriebsratkanzlei.legalconsenthub.user.UserRoleConverter +// import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto +// import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType +// import org.springframework.data.domain.Page +// import org.springframework.data.domain.PageRequest +// import org.springframework.stereotype.Service +// import org.springframework.transaction.annotation.Transactional +// import java.util.UUID +// +// @Service +// class NotificationService( +// private val notificationRepository: NotificationRepository, +// private val notificationMapper: NotificationMapper, +// private val userRepository: UserRepository, +// private val userRoleConverter: UserRoleConverter +// ) { +// +// fun createNotification(createNotificationDto: CreateNotificationDto): Notification { +// val notification = notificationMapper.toNotification(createNotificationDto) +// return notificationRepository.save(notification) +// } +// +// fun createNotification( +// title: String, +// message: String, +// clickTarget: String, +// recipient: User?, +// role: String, +// organizationId: String, +// type: NotificationType = NotificationType.INFO +// ): Notification { +// val notification = Notification( +// title = title, +// message = message, +// clickTarget = clickTarget, +// recipient = recipient, +// role = role, +// type = type, +// organizationId = organizationId +// ) +// return notificationRepository.save(notification) +// } +// +// fun getNotifications( +// recipientId: String, +// organizationId: String, +// page: Int = 0, +// size: Int = 20 +// ): Page { +// val user = userRepository.findById(recipientId) +// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } +// +// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) +// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } +// +// val pageable = PageRequest.of(page, size) +// return if (userRoles.isNotEmpty()) { +// notificationRepository.findUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs, pageable) +// } else { +// notificationRepository.findByRecipientIdOrderByCreatedAtDesc(recipientId, pageable) +// } +// } +// +// fun getUnreadNotifications( +// recipientId: String, +// organizationId: String +// ): List { +// val user = userRepository.findById(recipientId) +// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } +// +// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) +// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } +// +// return if (userRoles.isNotEmpty()) { +// notificationRepository.findUnreadUserNotificationsByOrgRole(recipientId, listOf(organizationId), orgRolePairs) +// } else { +// notificationRepository.findByRecipientIdAndIsReadFalseOrderByCreatedAtDesc(recipientId) +// } +// } +// +// fun getUnreadNotificationCount( +// recipientId: String, +// organizationId: String +// ): Long { +// val user = userRepository.findById(recipientId) +// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } +// +// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) +// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } +// +// return if (userRoles.isNotEmpty()) { +// notificationRepository.countUnreadUserNotifications(recipientId, listOf(organizationId), orgRolePairs) +// } else { +// notificationRepository.countByRecipientIdAndIsReadFalse(recipientId) +// } +// } +// +// @Transactional +// fun markAllAsRead( +// recipientId: String, +// organizationId: String +// ) { +// val user = userRepository.findById(recipientId) +// .orElseThrow { IllegalArgumentException("User not found with id: $recipientId") } +// +// val userRoles = userRoleConverter.getRolesForOrganization(user.organizationRoles, organizationId) +// val orgRolePairs = userRoles.map { role -> "$organizationId:${role.value}" } +// +// if (userRoles.isNotEmpty()) { +// notificationRepository.markAllUserNotificationsAsRead(recipientId, listOf(organizationId), orgRolePairs) +// } else { +// notificationRepository.markAllAsReadByRecipientId(recipientId) +// } +// } +// +// @Transactional +// fun markNotificationAsRead(notificationId: UUID): Notification { +// val notification = notificationRepository.findById(notificationId) +// .orElseThrow { IllegalArgumentException("Notification not found with id: $notificationId") } +// notification.isRead = true +// return notificationRepository.save(notification) +// } +// } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt index 9d0397a..f43a45b 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtAuthenticationConverter.kt @@ -11,12 +11,12 @@ class CustomJwtAuthenticationConverter : Converter = emptyList() - val userId = jwt.getClaimAsString("id") + val userId = jwt.subject val username = jwt.getClaimAsString("name") - val organizationId = jwt.getClaimAsString("organizationId") - val roles = jwt.getClaimAsStringList("roles") ?: emptyList() + val realmAccess = jwt.getClaimAsMap("realm_access") + val roles = (realmAccess?.get("roles") as? List<*>)?.mapNotNull { it as? String } ?: emptyList() - val principal = CustomJwtTokenPrincipal(userId, username, organizationId, roles) + val principal = CustomJwtTokenPrincipal(userId, username, roles) return CustomJwtAuthentication(jwt, principal, authorities) } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt index 77c8cb1..62837f6 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/CustomJwtTokenPrincipal.kt @@ -3,6 +3,6 @@ package com.betriebsratkanzlei.legalconsenthub.security data class CustomJwtTokenPrincipal( val id: String? = null, val name: String? = null, + val roles: List = emptyList(), val organizationId: String? = null, - val roles: List = emptyList() ) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt new file mode 100644 index 0000000..0c6dff5 --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt @@ -0,0 +1,55 @@ +package com.betriebsratkanzlei.legalconsenthub.security + +import com.betriebsratkanzlei.legalconsenthub.user.UserService +import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.LoggerFactory +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtUserSyncFilter(val userService: UserService) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val logger = LoggerFactory.getLogger(JwtUserSyncFilter::class.java) + val auth: Authentication? = SecurityContextHolder.getContext().authentication + + println("JwtUserSyncFilter invoked for request: ${request.requestURI}") + try { + if (auth is JwtAuthenticationToken && auth.isAuthenticated) { + val jwt: Jwt = auth.token + val keycloakId = jwt.subject + val name = jwt.getClaimAsString("name") + + // Extract organization information from JWT + val organizationClaim = jwt.getClaimAsMap("organization") + val organizationId = organizationClaim?.values?.firstOrNull()?.let { orgData -> + if (orgData is Map<*, *>) { + orgData["id"] as? String + } else { + null + } + } + + val user = UserDto(keycloakId, name, organizationId) + + if (keycloakId != null) { + userService.createUpdateUserFromJwt(user) + } + } + } catch (ex: Exception) { + logger.warn("Failed to sync user from JWT: {}", ex.message, ex) + } + + filterChain.doFilter(request, response) + } +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt index 0813661..a8bc10f 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/User.kt @@ -13,18 +13,13 @@ import java.time.LocalDateTime class User( @Id @Column(nullable = false) - var id: String, + var keycloakId: String, @Column(nullable = false) var name: String, - @Enumerated(EnumType.STRING) - @Column(nullable = false) - var status: UserStatus = UserStatus.ACTIVE, - - @ElementCollection - @CollectionTable(name = "user_organization_roles", joinColumns = [JoinColumn(name = "user_id")]) - var organizationRoles: MutableSet = mutableSetOf(), + @Column(nullable = true) + var organizationId: String? = null, @CreatedDate @Column(nullable = false) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserController.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserController.kt index 870f09e..2d397b3 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserController.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserController.kt @@ -1,11 +1,8 @@ package com.betriebsratkanzlei.legalconsenthub.user -import com.betriebsratkanzlei.legalconsenthub.security.CustomJwtTokenPrincipal 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.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.RestController @RestController @@ -13,33 +10,11 @@ class UserController( private val userService: UserService, private val userMapper: UserMapper ) : UserApi { - - override fun createUser(createUserDto: CreateUserDto): ResponseEntity { - val user = userService.createUser(createUserDto) - return ResponseEntity.status(201).body(userMapper.toUserDto(user)) - } - override fun getUserById(id: String): ResponseEntity { val user = userService.getUserById(id) return ResponseEntity.ok(userMapper.toUserDto(user)) } - override fun updateUser(id: String, userDto: UserDto?): ResponseEntity { - val user = if (userDto != null) { - // Update with provided data - userService.updateUser(id, userDto) - } else { - // Update from JWT data - val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal - val userId = principal.id ?: throw IllegalArgumentException("User ID missing from JWT") - val organizationId = principal.organizationId - val roles = principal.roles - - userService.updateUserFromJwt(userId, organizationId, roles) - } - return ResponseEntity.ok(userMapper.toUserDto(user)) - } - override fun deleteUser(id: String): ResponseEntity { userService.deleteUser(id) return ResponseEntity.noContent().build() diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt index 945c424..6592590 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserMapper.kt @@ -4,31 +4,22 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto import org.springframework.stereotype.Component @Component -class UserMapper( - private val roleConverter: UserRoleConverter -) { +class UserMapper { fun toUserDto(user: User): UserDto { - val organizationRolesDto = roleConverter.convertToMap(user.organizationRoles) - return UserDto( - id = user.id, + keycloakId = user.keycloakId, name = user.name, - status = user.status, - organizationRoles = organizationRolesDto + organizationId = user.organizationId ) } fun toUser(userDto: UserDto): User { val user = User( - id = userDto.id, + keycloakId = userDto.keycloakId, name = userDto.name, - status = userDto.status + organizationId = userDto.organizationId ) - userDto.organizationRoles.forEach { (orgId, roles) -> - roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles) - } - return user } } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserOrganizationRole.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserOrganizationRole.kt deleted file mode 100644 index c4df765..0000000 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserOrganizationRole.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.betriebsratkanzlei.legalconsenthub.user - -import jakarta.persistence.Column -import jakarta.persistence.Embeddable - -@Embeddable -data class UserOrganizationRole( - @Column(name = "organization_id", nullable = false) - val organizationId: String, - - @Column(name = "role", nullable = false) - val role: String -) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRoleConverter.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRoleConverter.kt deleted file mode 100644 index 688291b..0000000 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRoleConverter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.betriebsratkanzlei.legalconsenthub.user - -import com.betriebsratkanzlei.legalconsenthub_api.model.UserRole -import org.springframework.stereotype.Component - -@Component -object UserRoleConverter { - - fun getRolesForOrganization(organizationRoles: Set, organizationId: String): List { - return organizationRoles - .filter { it.organizationId == organizationId } - .mapNotNull { orgRole -> - try { - UserRole.valueOf(orgRole.role) - } catch (e: IllegalArgumentException) { - null - } - } - } - - fun setRolesForOrganization(organizationRoles: MutableSet, organizationId: String, roles: List) { - organizationRoles.removeIf { it.organizationId == organizationId } - roles.forEach { role -> - organizationRoles.add(UserOrganizationRole(organizationId, role.name)) - } - } - - fun convertToMap(organizationRoles: Set): Map> { - return organizationRoles - .groupBy { it.organizationId } - .mapValues { (_, roles) -> - roles.mapNotNull { orgRole -> - try { - UserRole.valueOf(orgRole.role) - } catch (e: IllegalArgumentException) { - null - } - } - } - .filterValues { it.isNotEmpty() } - } -} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserService.kt index f1f4a3f..e7288fd 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserService.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserService.kt @@ -3,9 +3,7 @@ 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.UserDto -import com.betriebsratkanzlei.legalconsenthub_api.model.UserStatus import jakarta.transaction.Transactional import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service @@ -13,7 +11,7 @@ import org.springframework.stereotype.Service @Service class UserService( private val userRepository: UserRepository, - private val roleConverter: UserRoleConverter + private val userMapper: UserMapper ) { fun getCurrentUser(): User { @@ -24,21 +22,32 @@ class UserService( .orElseThrow { UserNotFoundException(userId) } } - fun createUser(createUserDto: CreateUserDto): User { - if (userRepository.existsById(createUserDto.id)) { - throw UserAlreadyExistsException(createUserDto.id) + @Transactional + fun createUpdateUserFromJwt(userDto: UserDto): User { + val existingUser = userRepository.findById(userDto.keycloakId) + + if (existingUser.isEmpty) { + return createUser(userDto) + } else { + val user = existingUser.get() + if (user.organizationId == null && userDto.organizationId != null) { + user.organizationId = userDto.organizationId + } + return updateUser(userMapper.toUserDto(user)) + } + } + + fun createUser(userDto: UserDto): User { + if (userRepository.existsById(userDto.keycloakId)) { + throw UserAlreadyExistsException(userDto.keycloakId) } val user = User( - id = createUserDto.id, - name = createUserDto.name, - status = createUserDto.status + keycloakId = userDto.keycloakId, + name = userDto.name, + organizationId = userDto.organizationId ) - createUserDto.organizationRoles?.forEach { (orgId, roles) -> - roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles) - } - return userRepository.save(user) } @@ -48,43 +57,19 @@ class UserService( } @Transactional - fun updateUser(userId: String, userDto: UserDto): User { - val user = userRepository.findById(userId) - .orElseThrow { UserNotFoundException(userId) } + fun updateUser(userDto: UserDto): User { + val user = userRepository.findById(userDto.keycloakId) + .orElseThrow { UserNotFoundException(userDto.keycloakId) } user.name = userDto.name - user.status = userDto.status - - user.organizationRoles.clear() - userDto.organizationRoles.forEach { (orgId, roles) -> - roleConverter.setRolesForOrganization(user.organizationRoles, orgId, roles) + // Only update organization if it's not already set + if (user.organizationId == null && userDto.organizationId != null) { + user.organizationId = userDto.organizationId } return userRepository.save(user) } - @Transactional - fun updateUserFromJwt(userId: String, jwtOrganizationId: String?, jwtRoles: List?): User { - val existingUser = userRepository.findById(userId) - .orElseThrow { UserNotFoundException(userId) } - - if (jwtOrganizationId != null && !jwtRoles.isNullOrEmpty()) { - existingUser.organizationRoles.removeIf { it.organizationId == jwtOrganizationId } - - jwtRoles.forEach { role -> - val normalizedRole = role.lowercase().replace("_", "_") - existingUser.organizationRoles.add( - UserOrganizationRole( - organizationId = jwtOrganizationId, - role = normalizedRole - ) - ) - } - } - - return userRepository.save(existingUser) - } - fun deleteUser(userId: String) { userRepository.deleteById(userId) } diff --git a/legalconsenthub-backend/src/main/resources/application.yaml b/legalconsenthub-backend/src/main/resources/application.yaml index ad80bae..1adbfe0 100644 --- a/legalconsenthub-backend/src/main/resources/application.yaml +++ b/legalconsenthub-backend/src/main/resources/application.yaml @@ -32,6 +32,13 @@ spring: init: platform: h2 + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:7080/realms/legalconsenthub + jwk-set-uri: http://localhost:7080/realms/legalconsenthub/protocol/openid-connect/certs + logging: level: org: diff --git a/legalconsenthub/README.md b/legalconsenthub/README.md index 8acd148..5b4b9b9 100644 --- a/legalconsenthub/README.md +++ b/legalconsenthub/README.md @@ -1,39 +1,3 @@ # Legal Consent Hub ## Setup - -1. Create `.env` file with these variables: - ``` - BETTER_AUTH_URL=http://localhost:3000 - BETTER_AUTH_SECRET=YOUR_SECRET - ``` -2. Generate database schema: `pnpm dlx @better-auth/cli generate` -3. Migrate schema: `pnpm dlx @better-auth/cli migrate` - -## Common errors - -### better-auth/cli generate - -``` -Couldn't read your auth config. Error: Could not locate the bindings file. Tried: -``` - -**Solution:** I was able to resolve by running npx node-gyp rebuild in 'node_modules/better-sqlite3' - -https://github.com/WiseLibs/better-sqlite3/issues/1320 - -https://github.com/WiseLibs/better-sqlite3/issues/146 - -### This version of Node.js requires NODE_MODULE_VERSION 131. - -``` -rm -fr node_modules; pnpm store prune -``` - -https://github.com/elizaOS/eliza/pull/665 - -### Unauthorized /token and /organization/list endpoints - -User needs to be logged in to access these endpoints. - -https://www.better-auth.com/docs/plugins/organization#accept-invitation diff --git a/legalconsenthub/app.vue b/legalconsenthub/app.vue index 57b60a6..1d606f5 100644 --- a/legalconsenthub/app.vue +++ b/legalconsenthub/app.vue @@ -18,8 +18,8 @@ const color = computed(() => (colorMode.value === 'dark' ? '#111827' : 'white')) useHead({ meta: [ { charset: 'utf-8' }, - { name: 'viewport', content: 'width=device-width, initial-scale=1' }, - { key: 'theme-color', name: 'theme-color', content: color } + { userName: 'viewport', content: 'width=device-width, initial-scale=1' }, + { key: 'theme-color', userName: 'theme-color', content: color } ], link: [{ rel: 'icon', href: '/favicon.ico' }], htmlAttrs: { @@ -39,4 +39,8 @@ useSeoMeta({ twitterImage: 'https://dashboard-template.nuxt.dev/social-card.png', twitterCard: 'summary_large_image' }) + +// onBeforeMount(() => { +// $fetch('/api/auth/refresh') +// }) diff --git a/legalconsenthub/better-auth_migrations/2025-08-07T06-04-32.922Z.sql b/legalconsenthub/better-auth_migrations/2025-08-07T06-04-32.922Z.sql deleted file mode 100644 index 7089c34..0000000 --- a/legalconsenthub/better-auth_migrations/2025-08-07T06-04-32.922Z.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null); - -create table "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id"), "activeOrganizationId" text); - -create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); - -create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date, "updatedAt" date); - -create table "jwks" ("id" text not null primary key, "publicKey" text not null, "privateKey" text not null, "createdAt" date not null); - -create table "organization" ("id" text not null primary key, "name" text not null, "slug" text not null unique, "logo" text, "createdAt" date not null, "metadata" text); - -create table "member" ("id" text not null primary key, "organizationId" text not null references "organization" ("id"), "userId" text not null references "user" ("id"), "role" text not null, "createdAt" date not null); - -create table "invitation" ("id" text not null primary key, "organizationId" text not null references "organization" ("id"), "email" text not null, "role" text, "status" text not null, "expiresAt" date not null, "inviterId" text not null references "user" ("id")); \ No newline at end of file diff --git a/legalconsenthub/components/CreateOrganizationModal.vue b/legalconsenthub/components/CreateOrganizationModal.vue deleted file mode 100644 index 165eeb3..0000000 --- a/legalconsenthub/components/CreateOrganizationModal.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - diff --git a/legalconsenthub/components/InviteMemberModal.vue b/legalconsenthub/components/InviteMemberModal.vue deleted file mode 100644 index 920fd4a..0000000 --- a/legalconsenthub/components/InviteMemberModal.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - diff --git a/legalconsenthub/components/NotificationsSlideover.vue b/legalconsenthub/components/NotificationsSlideover.vue index 86de644..1a34b5a 100644 --- a/legalconsenthub/components/NotificationsSlideover.vue +++ b/legalconsenthub/components/NotificationsSlideover.vue @@ -80,13 +80,13 @@ const isOpen = computed({ set: (value) => emit('update:modelValue', value) }) -const { notifications, fetchNotifications, handleNotificationClick } = useNotification() - -watch(isOpen, async (newValue) => { - if (newValue) { - await fetchNotifications() - } -}) +// const { notifications, fetchNotifications, handleNotificationClick } = useNotification() +// +// watch(isOpen, async (newValue) => { +// if (newValue) { +// await fetchNotifications() +// } +// }) function onNotificationClick(notification: NotificationDto) { handleNotificationClick(notification) diff --git a/legalconsenthub/components/UserMenu.vue b/legalconsenthub/components/UserMenu.vue index 39038e2..703149a 100644 --- a/legalconsenthub/components/UserMenu.vue +++ b/legalconsenthub/components/UserMenu.vue @@ -60,13 +60,14 @@ const colors = [ ] const neutrals = ['slate', 'gray', 'zinc', 'neutral', 'stone'] -const { user: betterAuthUser, signOut } = await useAuth() +const userStore = useUserStore() +const { user: keyCloakUser } = storeToRefs(userStore) const user = ref({ - name: betterAuthUser.value?.name, + name: keyCloakUser.value.name, avatar: { src: '/_nuxt/public/favicon.ico', - alt: betterAuthUser.value?.name + alt: keyCloakUser.value.name } }) @@ -178,7 +179,7 @@ const items = computed(() => [ icon: 'i-lucide-log-out', async onSelect(e: Event) { e.preventDefault() - await signOut({ redirectTo: '/' }) + await navigateTo('/auth/logout', { external: true }) } } ] diff --git a/legalconsenthub/composables/applicationForm/useApplicationForm.ts b/legalconsenthub/composables/applicationForm/useApplicationForm.ts index ff5749b..0f40c3a 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationForm.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationForm.ts @@ -1,9 +1,4 @@ -import { - type CreateApplicationFormDto, - type ApplicationFormDto, - type PagedApplicationFormDto, - ResponseError -} from '~/.api-client' +import { type CreateApplicationFormDto, type ApplicationFormDto, type PagedApplicationFormDto } from '~/.api-client' import { useApplicationFormApi } from './useApplicationFormApi' export function useApplicationForm() { @@ -15,11 +10,7 @@ export function useApplicationForm() { try { return await applicationFormApi.createApplicationForm(createApplicationFormDto) } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error('Failed creating application form:', e.response) - } else { - console.error('Failed creating application form:', e) - } + console.error('Failed creating application form:', e) return Promise.reject(e) } } @@ -28,11 +19,7 @@ export function useApplicationForm() { try { return await applicationFormApi.getAllApplicationForms(organizationId) } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error('Failed retrieving application forms:', e.response) - } else { - console.error('Failed retrieving application forms:', e) - } + console.error('Failed retrieving application forms:', e, JSON.stringify(e)) return Promise.reject(e) } } @@ -41,11 +28,7 @@ export function useApplicationForm() { try { return await applicationFormApi.getApplicationFormById(id) } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error(`Failed retrieving application form with ID ${id}:`, e.response) - } else { - console.error(`Failed retrieving application form with ID ${id}:`, e) - } + console.error(`Failed retrieving application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -61,11 +44,7 @@ export function useApplicationForm() { try { return await applicationFormApi.updateApplicationForm(id, applicationFormDto) } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error(`Failed updating application form with ID ${id}:`, e.response) - } else { - console.error(`Failed updating application form with ID ${id}:`, e) - } + console.error(`Failed updating application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -74,11 +53,7 @@ export function useApplicationForm() { try { return await applicationFormApi.deleteApplicationFormById(id) } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error(`Failed deleting application form with ID ${id}:`, e.response) - } else { - console.error(`Failed deleting application form with ID ${id}:`, e) - } + console.error(`Failed deleting application form with ID ${id}:`, e) return Promise.reject(e) } } @@ -91,11 +66,7 @@ export function useApplicationForm() { try { return await applicationFormApi.submitApplicationForm(id) } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error(`Failed submitting application form with ID ${id}:`, e.response) - } else { - console.error(`Failed submitting application form with ID ${id}:`, e) - } + console.error(`Failed submitting application form with ID ${id}:`, e) return Promise.reject(e) } } diff --git a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts index b4b976c..c43725c 100644 --- a/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts +++ b/legalconsenthub/composables/applicationForm/useApplicationFormApi.ts @@ -6,18 +6,22 @@ import { type PagedApplicationFormDto } from '~/.api-client' import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' +import { wrappedFetchWrap } from '~/utils/wrappedFetch' export function useApplicationFormApi() { const appBaseUrl = useRuntimeConfig().app.baseURL - const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public - const { jwt } = useAuth() + const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public const basePath = withoutTrailingSlash( - cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath) + cleanDoubleSlashes( + import.meta.client + ? appBaseUrl + clientProxyBasePath + : useRequestURL().origin + clientProxyBasePath + serverApiBasePath + ) ) const applicationFormApiClient = new ApplicationFormApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) + new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }) ) async function createApplicationForm( diff --git a/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplate.ts b/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplate.ts index 3246627..a4ee6a0 100644 --- a/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplate.ts +++ b/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplate.ts @@ -8,8 +8,8 @@ import { useApplicationFormTemplateApi } from './useApplicationFormTemplateApi' const currentApplicationForm: Ref = ref() -export function useApplicationFormTemplate() { - const applicationFormApi = useApplicationFormTemplateApi() +export async function useApplicationFormTemplate() { + const applicationFormApi = await useApplicationFormTemplateApi() async function createApplicationFormTemplate( createApplicationFormDto: CreateApplicationFormDto diff --git a/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts b/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts index 38b748d..6306bf1 100644 --- a/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts +++ b/legalconsenthub/composables/applicationFormTemplate/useApplicationFormTemplateApi.ts @@ -1,18 +1,22 @@ import { ApplicationFormTemplateApi, Configuration } from '../../.api-client' import type { CreateApplicationFormDto, ApplicationFormDto, PagedApplicationFormDto } from '~/.api-client' import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' +import { wrappedFetchWrap } from '~/utils/wrappedFetch' -export function useApplicationFormTemplateApi() { +export async function useApplicationFormTemplateApi() { const appBaseUrl = useRuntimeConfig().app.baseURL - const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public - const { jwt } = useAuth() + const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public const basePath = withoutTrailingSlash( - cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath) + cleanDoubleSlashes( + import.meta.client + ? appBaseUrl + clientProxyBasePath + : useRequestURL().origin + clientProxyBasePath + serverApiBasePath + ) ) const applicationFormApiClient = new ApplicationFormTemplateApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) + new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }) ) async function createApplicationFormTemplate( diff --git a/legalconsenthub/composables/auth/useAuthActions.ts b/legalconsenthub/composables/auth/useAuthActions.ts deleted file mode 100644 index f3dab28..0000000 --- a/legalconsenthub/composables/auth/useAuthActions.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { FormSubmitEvent } from '@nuxt/ui' -import type { SignInSchema, SignUpSchema } from '~/types/schemas' -import type { RouteLocationRaw } from 'vue-router' -import { useAuthClient } from '~/composables/auth/useAuthClient' -import { useAuthState } from '~/composables/auth/useAuthState' - -export function useAuthActions() { - const { client } = useAuthClient() - const { session, user } = useAuthState() - const { createUser } = useUser() - const toast = useToast() - - async function signOut({ redirectTo }: { redirectTo?: RouteLocationRaw } = {}) { - const res = await client.signOut() - if (res.error) { - console.error('Error signing out:', res.error) - return res - } - session.value = null - user.value = null - if (redirectTo) { - await navigateTo(redirectTo, { external: true }) - } - return res - } - - async function signUp(payload: FormSubmitEvent) { - await client.signUp.email( - { - email: payload.data.email, - password: payload.data.password, - name: payload.data.name - }, - { - onRequest: () => { - console.log('Sending register request') - }, - onResponse: () => { - console.log('Receiving register response') - }, - onSuccess: async (ctx) => { - 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' - }) - 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('/') - }, - onError: async (ctx) => { - console.log(ctx.error.message) - toast.add({ - title: 'Fehler bei der Registrierung', - description: ctx.error.message, - color: 'error' - }) - } - } - ) - } - - async function signIn(payload: FormSubmitEvent) { - await client.signIn.email( - { - email: payload.data.email, - password: payload.data.password - }, - { - onRequest: () => { - console.log('Sending login request') - }, - onSuccess: async () => { - console.log('Successfully logged in!') - await navigateTo('/') - }, - onError: (ctx) => { - console.log(ctx.error.message) - toast.add({ - title: 'Fehler bei der Anmeldung', - description: ctx.error.message, - color: 'error' - }) - } - } - ) - } - - return { - signOut, - signUp, - signIn - } -} diff --git a/legalconsenthub/composables/auth/useAuthClient.ts b/legalconsenthub/composables/auth/useAuthClient.ts deleted file mode 100644 index 55bb940..0000000 --- a/legalconsenthub/composables/auth/useAuthClient.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createAuthClient } from 'better-auth/vue' -import { jwtClient, organizationClient } from 'better-auth/client/plugins' -import { - accessControl, - adminRole, - employeeRole, - employerRole, - ownerRole, - ROLES, - worksCouncilMemberRole -} from '~/server/utils/permissions' - -export function useAuthClient() { - const url = useRequestURL() - const headers = import.meta.server ? useRequestHeaders() : undefined - - const client = createAuthClient({ - baseURL: url.origin, - fetchOptions: { - headers - }, - user: { - deleteUser: { - enabled: true - } - }, - plugins: [ - organizationClient({ - // Pass the same access control instance and roles to client - ac: accessControl, - roles: { - [ROLES.EMPLOYER]: employerRole, - [ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole, - [ROLES.EMPLOYEE]: employeeRole, - [ROLES.ADMIN]: adminRole, - [ROLES.OWNER]: ownerRole - } - }), - jwtClient() - ] - }) - - return { - client - } -} diff --git a/legalconsenthub/composables/auth/useAuthState.ts b/legalconsenthub/composables/auth/useAuthState.ts deleted file mode 100644 index 000f4a1..0000000 --- a/legalconsenthub/composables/auth/useAuthState.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { ClientOptions, InferSessionFromClient, InferUserFromClient } from 'better-auth/client' -import type { RuntimeAuthConfig } from '~/types/auth' -import { defu } from 'defu' -import { useAuthClient } from '~/composables/auth/useAuthClient' - -// Global state for auth -const session = ref | null>(null) -const user = ref | null>(null) -const sessionFetching = import.meta.server ? ref(false) : ref(false) -const jwt = ref(null) -const organizations = ref< - { - id: string - name: string - createdAt: Date - slug: string - metadata?: Record - logo?: string | null - }[] ->([]) -const selectedOrganization = ref<{ - id: string - name: string - createdAt: Date - slug: string - metadata?: Record - logo?: string | null -} | null>(null) -const activeMember = ref<{ role: string } | null>(null) - -export function useAuthState() { - const { client } = useAuthClient() - const route = useRoute() - - const options = defu(useRuntimeConfig().public.auth as Partial, { - redirectUserTo: '/', - redirectGuestTo: '/login' - }) - - const headers = import.meta.server ? useRequestHeaders() : undefined - - async function fetchSession(targetPath?: string) { - if (sessionFetching.value) { - console.log('already fetching session') - return - } - sessionFetching.value = true - const { data } = await client.getSession({ - fetchOptions: { - headers - } - }) - session.value = data?.session || null - user.value = data?.user || null - sessionFetching.value = false - - // Only fetch JWT and organizations if we have a session and not on public routes - if (session.value && !isPublicPath(targetPath)) { - await fetchJwtAndOrganizations() - } - - return data - } - - async function fetchJwtAndOrganizations() { - // Fetch JWT - const tokenResult = await client.token() - jwt.value = tokenResult.data?.token ?? null - - // Fetch organization - const orgResult = await client.organization.list({ - fetchOptions: { - headers - } - }) - organizations.value = orgResult.data ?? [] - - if (!selectedOrganization.value && organizations.value.length > 0) { - selectedOrganization.value = organizations.value[0] - } - - // Fetch active member - const activeMemberResult = await client.organization.getActiveMember({ - fetchOptions: { - headers - } - }) - activeMember.value = activeMemberResult.data || null - } - - function isPublicPath(path?: string) { - const finalPath = path ?? route.path - const publicRoutes = ['/login', '/signup', '/accept-invitation'] - return publicRoutes.some((path) => finalPath.startsWith(path)) - } - - // Watch organization changes - watch( - () => selectedOrganization.value, - async (newValue) => { - if (newValue) { - await client.organization.setActive({ - organizationId: newValue?.id - }) - } - } - ) - - // Client-side session listening - if (import.meta.client) { - client.$store.listen('$sessionSignal', async (signal) => { - if (!signal) return - await fetchSession() - }) - } - - return { - session, - user, - sessionFetching, - jwt, - organizations, - selectedOrganization, - activeMember, - options, - fetchSession, - isPublicPath, - loggedIn: computed(() => !!session.value) - } -} diff --git a/legalconsenthub/composables/comment/useCommentApi.ts b/legalconsenthub/composables/comment/useCommentApi.ts index b2baaab..c3ad1a2 100644 --- a/legalconsenthub/composables/comment/useCommentApi.ts +++ b/legalconsenthub/composables/comment/useCommentApi.ts @@ -1,17 +1,21 @@ import { CommentApi, Configuration, type CommentDto, type CreateCommentDto, type PagedCommentDto } from '~/.api-client' import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' +import { wrappedFetchWrap } from '~/utils/wrappedFetch' export function useCommentApi() { const appBaseUrl = useRuntimeConfig().app.baseURL - const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public - const { jwt } = useAuth() + const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public const basePath = withoutTrailingSlash( - cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath) + cleanDoubleSlashes( + import.meta.client + ? appBaseUrl + clientProxyBasePath + : useRequestURL().origin + clientProxyBasePath + serverApiBasePath + ) ) const commentApiClient = new CommentApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) + new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }) ) async function createComment( diff --git a/legalconsenthub/composables/comment/useCommentTextarea.ts b/legalconsenthub/composables/comment/useCommentTextarea.ts index ca4d019..e75c57f 100644 --- a/legalconsenthub/composables/comment/useCommentTextarea.ts +++ b/legalconsenthub/composables/comment/useCommentTextarea.ts @@ -3,7 +3,8 @@ import type { CreateCommentDto, CommentDto } from '~/.api-client' export function useCommentTextarea(applicationFormId: string) { const commentStore = useCommentStore() const { createComment, updateComment } = commentStore - const { user } = useAuth() + const userStore = useUserStore() + const { user } = storeToRefs(userStore) const isEditingComment = ref(false) const currentEditedComment = ref(null) const commentTextAreaValue = ref('') @@ -51,7 +52,7 @@ export function useCommentTextarea(applicationFormId: string) { } function isCommentByUser(comment: CommentDto) { - return comment.createdBy.id === user.value?.id + return comment.createdBy.keycloakId === user.value?.keycloakId } return { diff --git a/legalconsenthub/composables/index.ts b/legalconsenthub/composables/index.ts index 91e5b68..6991f5a 100644 --- a/legalconsenthub/composables/index.ts +++ b/legalconsenthub/composables/index.ts @@ -2,5 +2,3 @@ export { useApplicationFormTemplate } from './applicationFormTemplate/useApplica 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' diff --git a/legalconsenthub/composables/middleware/useMiddleware.ts b/legalconsenthub/composables/middleware/useMiddleware.ts deleted file mode 100644 index d096012..0000000 --- a/legalconsenthub/composables/middleware/useMiddleware.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { - VerifySignatureHashAlgorithmEnum, - VerifySignatureResponseDto, - SignPdfHashHashAlgorithmEnum -} from '~/.api-client-middleware' -import { useMiddlewareApi } from '~/composables/middleware/useMiddlewareApi' - -export function useMiddleware() { - const middlewareApi = useMiddlewareApi() - - async function signPdfHash(document: Blob, certificateId: string, hashAlgorithm?: SignPdfHashHashAlgorithmEnum) { - try { - return middlewareApi.signPdfHash(document, certificateId, hashAlgorithm) - } catch (e: unknown) { - console.error('Failed signing PDF hash:', e) - return Promise.reject(e) - } - } - - async function verifySignature( - document: Blob, - signature: string, - certificateId?: string, - hashAlgorithm?: VerifySignatureHashAlgorithmEnum - ): Promise { - try { - return await middlewareApi.verifySignature(document, signature, certificateId, hashAlgorithm) - } catch (e: unknown) { - console.error('Failed verifying signature:', e) - return Promise.reject(e) - } - } - - return { - signPdfHash, - verifySignature - } -} diff --git a/legalconsenthub/composables/middleware/useMiddlewareApi.ts b/legalconsenthub/composables/middleware/useMiddlewareApi.ts deleted file mode 100644 index e6508d2..0000000 --- a/legalconsenthub/composables/middleware/useMiddlewareApi.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' -import { - SmartCardApi, - SignatureApi, - Configuration, - type VerifySignatureHashAlgorithmEnum, - type VerifySignatureResponseDto, - type SignPdfHashHashAlgorithmEnum -} from '~/.api-client-middleware' - -export function useMiddlewareApi() { - 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 smartCardApiClient = new SmartCardApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) - ) - - const signatureApiClient = new SignatureApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) - ) - - async function signPdfHash(document: Blob, certificateId: string, hashAlgorithm?: SignPdfHashHashAlgorithmEnum) { - return signatureApiClient.signPdfHash({ document, certificateId, hashAlgorithm }) - } - - async function verifySignature( - document: Blob, - signature: string, - certificateId?: string, - hashAlgorithm?: VerifySignatureHashAlgorithmEnum - ): Promise { - return signatureApiClient.verifySignature({ document, signature, certificateId, hashAlgorithm }) - } - - return { - signPdfHash, - verifySignature - } -} diff --git a/legalconsenthub/composables/notification/useNotificationApi.ts b/legalconsenthub/composables/notification/useNotificationApi.ts index db3a749..97dca0b 100644 --- a/legalconsenthub/composables/notification/useNotificationApi.ts +++ b/legalconsenthub/composables/notification/useNotificationApi.ts @@ -6,18 +6,22 @@ import { type CreateNotificationDto } from '~/.api-client' import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' +import { wrappedFetchWrap } from '~/utils/wrappedFetch' export function useNotificationApi() { const appBaseUrl = useRuntimeConfig().app.baseURL - const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public - const { jwt } = useAuth() + const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public const basePath = withoutTrailingSlash( - cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath) + cleanDoubleSlashes( + import.meta.client + ? appBaseUrl + clientProxyBasePath + : useRequestURL().origin + clientProxyBasePath + serverApiBasePath + ) ) const notificationApiClient = new NotificationApi( - new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } }) + new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }) ) async function createNotification(createNotificationDto: CreateNotificationDto): Promise { diff --git a/legalconsenthub/composables/organization/useOrganizationApi.ts b/legalconsenthub/composables/organization/useOrganizationApi.ts deleted file mode 100644 index eda8cac..0000000 --- a/legalconsenthub/composables/organization/useOrganizationApi.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { LegalRole } from '~/server/utils/permissions' -import type { ListMembersOptions } from '~/types/auth' - -export function useOrganizationApi() { - const { organization } = useAuth() - - async function createOrganization(name: string, slug: string, logo?: string) { - return organization.create({ name, slug, logo }) - } - - async function deleteOrganization(organizationId: string) { - return organization.delete({ organizationId }) - } - - async function getInvitation(invitationId: string) { - return organization.getInvitation({ query: { id: invitationId } }) - } - - async function listInvitations(organizationId?: string) { - return organization.listInvitations(organizationId ? { query: { organizationId: organizationId } } : undefined) - } - - async function inviteMember(email: string, role: LegalRole) { - return organization.inviteMember({ email, role }) - } - - async function removeMember(memberIdOrEmail: string) { - return organization.removeMember({ memberIdOrEmail }) - } - - async function acceptInvitation(invitationId: string) { - return organization.acceptInvitation({ invitationId }) - } - - async function cancelSentInvitation(invitationId: string) { - return organization.cancelInvitation({ invitationId }) - } - - async function rejectInvitation(invitationId: string) { - return organization.rejectInvitation({ invitationId }) - } - - async function loadOrganizations() { - return organization.list() - } - - async function checkSlugAvailability(slug: string) { - return organization.checkSlug({ slug }) - } - - async function setActiveOrganization(organizationId: string) { - return organization.setActive({ organizationId }) - } - - async function listMembers(options?: ListMembersOptions) { - return organization.listMembers(options) - } - - return { - createOrganization, - deleteOrganization, - getInvitation, - listInvitations, - inviteMember, - removeMember, - acceptInvitation, - cancelSentInvitation, - rejectInvitation, - loadOrganizations, - checkSlugAvailability, - setActiveOrganization, - listMembers - } -} diff --git a/legalconsenthub/composables/useAuth.ts b/legalconsenthub/composables/useAuth.ts deleted file mode 100644 index 285029d..0000000 --- a/legalconsenthub/composables/useAuth.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copied from https://github.com/atinux/nuxthub-better-auth - -import { useAuthState } from '~/composables/auth/useAuthState' -import { useAuthActions } from '~/composables/auth/useAuthActions' -import { useAuthClient } from '~/composables/auth/useAuthClient' - -export function useAuth() { - const authState = useAuthState() - const authActions = useAuthActions() - const { client } = useAuthClient() - - return { - ...authState, - ...authActions, - client, - deleteUser: client.deleteUser, - organization: client.organization - } -} diff --git a/legalconsenthub/composables/usePermissions.ts b/legalconsenthub/composables/usePermissions.ts deleted file mode 100644 index 0b4bd58..0000000 --- a/legalconsenthub/composables/usePermissions.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { ROLES, type LegalRole } from '~/server/utils/permissions' - -export function usePermissions() { - const { organization, activeMember } = useAuth() - - const currentRole = computed((): LegalRole | null => { - return (activeMember.value?.role as LegalRole) || null - }) - - const hasPermission = (permissions: Record): boolean => { - if (!currentRole.value) return false - - return organization.checkRolePermission({ - permissions, - role: currentRole.value - }) - } - - // Specific permission helpers - const canCreateApplicationForm = computed(() => - hasPermission({ application_form: ["create"] }) - ) - - const canApproveApplicationForm = computed(() => - hasPermission({ application_form: ["approve"] }) - ) - - const canSignAgreement = computed(() => - hasPermission({ agreement: ["sign"] }) - ) - - const canInviteMembers = computed(() => - hasPermission({ invitation: ["create"] }) - ) - - const canManageOrganization = computed(() => - hasPermission({ organization: ["update"] }) - ) - - // Role checks - const isEmployer = computed(() => currentRole.value === ROLES.EMPLOYER) - const isEmployee = computed(() => currentRole.value === ROLES.EMPLOYEE) - const isWorksCouncilMember = computed(() => currentRole.value === ROLES.WORKS_COUNCIL_MEMBER) - const isAdmin = computed(() => currentRole.value === ROLES.ADMIN) - const isOwner = computed(() => currentRole.value === ROLES.OWNER) - - const getCurrentRoleInfo = () => { - const roleInfo = { - [ROLES.EMPLOYER]: { - name: 'Arbeitgeber', - description: 'Kann Anträge genehmigen und Vereinbarungen unterzeichnen', - color: 'blue', - icon: 'i-lucide-briefcase' - }, - [ROLES.EMPLOYEE]: { - name: 'Arbeitnehmer', - description: 'Kann eigene Anträge einsehen und kommentieren', - color: 'green', - icon: 'i-lucide-user' - }, - [ROLES.WORKS_COUNCIL_MEMBER]: { - name: 'Betriebsrat', - description: 'Kann Anträge prüfen und Vereinbarungen unterzeichnen', - color: 'purple', - icon: 'i-lucide-users' - }, - [ROLES.ADMIN]: { - name: 'Administrator', - description: 'Vollzugriff auf Organisationsverwaltung', - color: 'red', - icon: 'i-lucide-settings' - }, - [ROLES.OWNER]: { - name: 'Eigentümer', - description: 'Vollzugriff und Organisationsbesitz', - color: 'yellow', - icon: 'i-lucide-crown' - } - } - - return currentRole.value && currentRole.value in roleInfo ? roleInfo[currentRole.value as LegalRole] : null - } - - return { - // State - currentRole, - activeMember, - - // Permission checks - hasPermission, - - // Role checks - isEmployer, - isEmployee, - isWorksCouncilMember, - isAdmin, - isOwner, - - // Computed permissions - canCreateApplicationForm, - canApproveApplicationForm, - canSignAgreement, - canInviteMembers, - canManageOrganization, - - // Utilities - getCurrentRoleInfo, - ROLES - } -} diff --git a/legalconsenthub/composables/user/useUser.ts b/legalconsenthub/composables/user/useUser.ts deleted file mode 100644 index daf9299..0000000 --- a/legalconsenthub/composables/user/useUser.ts +++ /dev/null @@ -1,72 +0,0 @@ -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 { - 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 { - 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 updateUser(id: string, userDto?: UserDto): Promise { - try { - return await userApi.updateUser(id, userDto) - } catch (e: unknown) { - if (e instanceof ResponseError) { - console.error(`Failed updating user with ID ${id}:`, e.response) - } else { - console.error(`Failed updating user with ID ${id}:`, e) - } - return Promise.reject(e) - } - } - - async function deleteUser(id: string): Promise { - 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, - updateUser, - deleteUser - } -} \ No newline at end of file diff --git a/legalconsenthub/composables/user/useUserApi.ts b/legalconsenthub/composables/user/useUserApi.ts deleted file mode 100644 index 627d63d..0000000 --- a/legalconsenthub/composables/user/useUserApi.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserApi, Configuration, type CreateUserDto, type UserDto } from '~/.api-client' -import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' -import { useAuthState } from '~/composables/auth/useAuthState' - -export function useUserApi() { - const appBaseUrl = useRuntimeConfig().app.baseURL - const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public - const { jwt } = useAuthState() - - const basePath = withoutTrailingSlash( - cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath) - ) - - // Track changing JWT of user who accepts the invitation - const userApiClient = computed( - () => - new UserApi(new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })) - ) - - async function createUser(createUserDto: CreateUserDto): Promise { - return userApiClient.value.createUser({ createUserDto }) - } - - async function getUserById(id: string): Promise { - return userApiClient.value.getUserById({ id }) - } - - async function updateUser(id: string, userDto?: UserDto): Promise { - return userApiClient.value.updateUser({ id, userDto }) - } - - async function deleteUser(id: string): Promise { - return userApiClient.value.deleteUser({ id }) - } - - return { - createUser, - getUserById, - updateUser, - deleteUser - } -} diff --git a/legalconsenthub/layouts/auth.vue b/legalconsenthub/layouts/auth.vue index 3195793..653ed1b 100644 --- a/legalconsenthub/layouts/auth.vue +++ b/legalconsenthub/layouts/auth.vue @@ -1,16 +1,5 @@ diff --git a/legalconsenthub/layouts/default.vue b/legalconsenthub/layouts/default.vue index 33cbea2..dd548c6 100644 --- a/legalconsenthub/layouts/default.vue +++ b/legalconsenthub/layouts/default.vue @@ -25,6 +25,7 @@ @@ -41,13 +42,25 @@ const open = ref(false) const isNotificationsSlideoverOpen = ref(false) const { unreadCount, fetchUnreadCount, startPeriodicRefresh } = useNotification() -onMounted(async () => { - await fetchUnreadCount() - startPeriodicRefresh() -}) +// onMounted(async () => { +// await fetchUnreadCount() +// startPeriodicRefresh() +// }) provide('notificationState', { isNotificationsSlideoverOpen, unreadCount }) + +async function copyAccessTokenToClipboard() { + const { session } = useUserSession() + console.log('Access Token :', session.value?.jwt?.accessToken) + const accessToken = session.value?.jwt?.accessToken + if (accessToken) { + navigator.clipboard.writeText(accessToken) + console.log('Access token copied to clipboard') + } else { + console.warn('No access token found in session') + } +} diff --git a/legalconsenthub/middleware/auth.global.ts b/legalconsenthub/middleware/auth.global.ts index dfea8e9..3c833b2 100644 --- a/legalconsenthub/middleware/auth.global.ts +++ b/legalconsenthub/middleware/auth.global.ts @@ -1,75 +1,14 @@ -// Copied from https://github.com/atinux/nuxthub-better-auth - -import { defu } from 'defu' import type { RouteLocationNormalized } from '#vue-router' -type MiddlewareOptions = - | false - | { - /** - * Only apply auth middleware to guest or user - */ - only?: 'guest' | 'user' - /** - * Redirect authenticated user to this route - */ - redirectUserTo?: string - /** - * Redirect guest to this route - */ - redirectGuestTo?: string - } - -declare module '#app' { - interface PageMeta { - auth?: MiddlewareOptions - } -} - -declare module 'vue-router' { - interface RouteMeta { - auth?: MiddlewareOptions - } -} - export default defineNuxtRouteMiddleware(async (to: RouteLocationNormalized) => { - // 1. If auth is disabled, skip middleware - if (to.meta?.auth === false) { - console.log('[1] Auth middleware disabled for this route:', to.path) + // https://github.com/WaldemarEnns/nuxtui-github-auth/blob/7e3110f933d5d0445d3ac89d6c84c48052b49041/middleware/auth.global.ts + const { loggedIn } = useUserSession() + + if (to.meta.auth === false) { return } - const { loggedIn, options, fetchSession, isPublicPath } = useAuth() - const { only, redirectUserTo, redirectGuestTo } = defu(to.meta?.auth, options) - // 2. If guest mode, redirect if authenticated - if (only === 'guest' && loggedIn.value) { - console.log('[2] Guest mode: user is authenticated, redirecting to', redirectUserTo) - if (to.path === redirectUserTo) { - console.log('[2.1] Already at redirectUserTo:', redirectUserTo) - return - } - return navigateTo(redirectUserTo) - } - - // 3. If client-side, fetch session between each navigation - if (import.meta.client) { - console.log('[3] Client-side navigation, fetching session') - try { - await fetchSession(to.path) - } catch (e) { - console.error(e) - } - } - - // 4. If not authenticated, redirect to home or guest route if (!loggedIn.value) { - if (isPublicPath(to.path)) { - console.log('[4] Not authenticated, but route is public:', to.path) - // Continue navigating to the public route - return - } - // No public route, redirect to guest route - console.log('[4.1] Not authenticated, redirecting to guest route:', redirectGuestTo) - return navigateTo(redirectGuestTo) + return navigateTo('/login') } }) diff --git a/legalconsenthub/middleware/refreshToken.global.ts b/legalconsenthub/middleware/refreshToken.global.ts new file mode 100644 index 0000000..f29caee --- /dev/null +++ b/legalconsenthub/middleware/refreshToken.global.ts @@ -0,0 +1,83 @@ +// Copied from https://github.com/atinux/nuxt-auth-utils/issues/91#issuecomment-2476019136 + +import { appendResponseHeader } from 'h3' +import { parse, parseSetCookie, serialize } from 'cookie-es' +import { jwtDecode, type JwtPayload } from 'jwt-decode' + +export default defineNuxtRouteMiddleware(async (to, from) => { + const nuxtApp = useNuxtApp() + // Don't run on client hydration when server rendered + if (import.meta.client && nuxtApp.isHydrating && nuxtApp.payload.serverRendered) return + + console.log('🔍 Middleware: refreshToken.global.ts') + console.log(` from: ${from.fullPath} to: ${to.fullPath}`) + + const { session, clear: clearSession, fetch: fetchSession } = useUserSession() + // Ignore if no tokens + if (!session.value?.jwt) return + + const serverEvent = useRequestEvent() + const runtimeConfig = useRuntimeConfig() + const { accessToken, refreshToken } = session.value.jwt + + const accessPayload = jwtDecode(accessToken) + const refreshPayload = jwtDecode(refreshToken) + + // Both tokens expired, clearing session + if (isExpired(accessPayload) && isExpired(refreshPayload)) { + console.info('both tokens expired, clearing session') + await clearSession() + return navigateTo('/login') + } else if (isExpired(accessPayload)) { + console.info('access token expired, refreshing') + await useRequestFetch()('/api/jwt/refresh', { + method: 'POST', + onResponse({ response: { headers } }) { + // Forward the Set-Cookie header to the main server event + if (import.meta.server && serverEvent) { + for (const setCookie of headers.getSetCookie()) { + appendResponseHeader(serverEvent, 'Set-Cookie', setCookie) + // Update session cookie for next fetch requests + const { name, value } = parseSetCookie(setCookie) + + if (name === runtimeConfig.session.name) { + console.log('updating headers.cookie to', value) + const cookies = parse(serverEvent.headers.get('cookie') || '') + + // set or overwrite existing cookie + cookies[name] = value + + // update cookie event header for future requests + serverEvent.headers.set( + 'cookie', + Object.entries(cookies) + .map(([name, value]) => serialize(name, value)) + .join('; ') + ) + + // Also apply to serverEvent.node.req.headers + if (serverEvent.node?.req?.headers) { + serverEvent.node.req.headers['cookie'] = serverEvent.headers.get('cookie') || '' + } + } + } + } + }, + onError() { + console.error('🔍 Middleware: Token refresh failed') + const { loggedIn } = useUserSession() + if (!loggedIn.value) { + console.log('🔍 Middleware: User not logged in, redirecting to /login') + return navigateTo('/login') + } + } + }) + + // Refresh the session + await fetchSession() + } +}) + +function isExpired(payload: JwtPayload) { + return payload?.exp && payload.exp < Date.now() / 1000 +} diff --git a/legalconsenthub/nuxt.config.ts b/legalconsenthub/nuxt.config.ts index ee642ae..29432bb 100644 --- a/legalconsenthub/nuxt.config.ts +++ b/legalconsenthub/nuxt.config.ts @@ -1,20 +1,21 @@ export default defineNuxtConfig({ sourcemap: true, - modules: [ - '@nuxt/ui-pro', - '@nuxt/eslint', - '@pinia/nuxt', - '@nuxtjs/i18n' - ], + modules: ['@nuxt/ui-pro', '@nuxt/eslint', '@pinia/nuxt', '@nuxtjs/i18n', 'nuxt-auth-utils'], css: ['~/assets/css/main.css'], runtimeConfig: { public: { clientProxyBasePath: 'NOT_SET', serverApiBaseUrl: 'NOT_SET', - serverApiBasePath: 'NOT_SET', - auth: { - redirectUserTo: '/', - redirectGuestTo: '/login' + serverApiBasePath: 'NOT_SET' + }, + oauth: { + keycloak: { + clientId: 'legalconsenthub', + clientSecret: 'mROUAVlg3c0hepNt182FJgg6dEYsomc7', + realm: 'legalconsenthub', + serverUrl: 'http://localhost:7080', + redirectURL: 'http://localhost:3001/auth/keycloak', + scope: ['openid', 'organization'] } } }, diff --git a/legalconsenthub/package.json b/legalconsenthub/package.json index 1646f37..e4b677b 100644 --- a/legalconsenthub/package.json +++ b/legalconsenthub/package.json @@ -7,26 +7,23 @@ "dev": "nuxt dev --port 3001 --host", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare && pnpm run fix:bettersqlite && pnpm run api:generate && pnpm run api:middleware:generate", + "postinstall": "nuxt prepare && pnpm run api:generate", "format": "prettier . --write", "type-check": "nuxi typecheck", "lint": "eslint .", "lint:fix": "eslint . --fix", "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", - "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 --yes", - "migrate:betterauth": "pnpm dlx @better-auth/cli migrate --config server/utils/auth.ts --yes", - "recreate-db:betterauth": "[ -f sqlite.db ] && rm sqlite.db; pnpm run migrate:betterauth" + "api:middleware:generate": "openapi-generator-cli generate -i ../legalconsenthub-middleware/api/legalconsenthub-middleware.yml -g typescript-fetch -o .api-client-middleware" }, "dependencies": { "@nuxt/ui-pro": "3.1.1", "@nuxtjs/i18n": "10.0.3", "@pinia/nuxt": "0.10.1", "@vueuse/core": "^13.6.0", - "better-auth": "1.3.9", - "better-sqlite3": "11.8.1", + "h3": "1.15.4", + "jwt-decode": "4.0.0", "nuxt": "3.16.1", + "nuxt-auth-utils": "0.5.25", "pinia": "3.0.1", "resend": "^4.3.0", "vue": "latest", @@ -35,7 +32,6 @@ "devDependencies": { "@nuxt/eslint": "1.1.0", "@openapitools/openapi-generator-cli": "2.16.3", - "@types/better-sqlite3": "7.6.12", "eslint": "9.20.1", "prettier": "3.5.1", "typescript": "5.7.3", diff --git a/legalconsenthub/pages/accept-invitation/[id].vue b/legalconsenthub/pages/accept-invitation/[id].vue deleted file mode 100644 index a17cadc..0000000 --- a/legalconsenthub/pages/accept-invitation/[id].vue +++ /dev/null @@ -1,92 +0,0 @@ -