From b72d5648685eddc30ccda53bbfbf28ded66a219e Mon Sep 17 00:00:00 2001 From: Denis Lugowski Date: Sun, 23 Nov 2025 18:46:14 +0100 Subject: [PATCH] feat(#23): Add email notifications --- api/legalconsenthub.yml | 56 ++++++++++++++ deployment/.env.example | 4 + deployment/docker-compose-dev.yaml | 9 +++ deployment/docker-compose-prod.yaml | 9 +++ legalconsenthub-backend/build.gradle | 1 + .../LegalconsenthubApplication.kt | 2 + .../ApplicationFormService.kt | 22 ++++++ .../email/ApplicationFormCreatedEvent.kt | 10 +++ .../email/ApplicationFormSubmittedEvent.kt | 10 +++ .../email/EmailEventListener.kt | 73 ++++++++++++++++++ .../legalconsenthub/email/EmailService.kt | 63 +++++++++++++++ .../security/JwtUserSyncFilter.kt | 11 ++- .../legalconsenthub/user/User.kt | 6 ++ .../legalconsenthub/user/UserController.kt | 15 ++++ .../legalconsenthub/user/UserMapper.kt | 6 ++ .../legalconsenthub/user/UserRepository.kt | 6 +- .../legalconsenthub/user/UserService.kt | 42 ++++++---- .../src/main/resources/application.yaml | 18 +++++ .../templates/email/form_created.html | 35 +++++++++ .../templates/email/form_submitted.html | 35 +++++++++ legalconsenthub/app/composables/index.ts | 2 + .../app/composables/user/useUser.ts | 26 +++++++ .../app/composables/user/useUserApi.ts | 42 ++++++++++ legalconsenthub/app/pages/settings.vue | 76 +++++++++++++++++++ legalconsenthub/i18n/locales/de.json | 26 +++++++ legalconsenthub/i18n/locales/en.json | 26 +++++++ 26 files changed, 613 insertions(+), 18 deletions(-) create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormCreatedEvent.kt create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormSubmittedEvent.kt create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailEventListener.kt create mode 100644 legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailService.kt create mode 100644 legalconsenthub-backend/src/main/resources/templates/email/form_created.html create mode 100644 legalconsenthub-backend/src/main/resources/templates/email/form_submitted.html create mode 100644 legalconsenthub/app/composables/user/useUser.ts create mode 100644 legalconsenthub/app/composables/user/useUserApi.ts diff --git a/api/legalconsenthub.yml b/api/legalconsenthub.yml index 31a6ed8..d062b91 100644 --- a/api/legalconsenthub.yml +++ b/api/legalconsenthub.yml @@ -539,6 +539,42 @@ paths: "503": $ref: "https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable" + /users/{id}/email-preferences: + parameters: + - name: id + in: path + required: true + schema: + type: string + put: + summary: Update user email preferences + operationId: updateUserEmailPreferences + tags: + - user + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateEmailPreferencesDto" + responses: + "200": + description: Email preferences updated successfully + 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" + ####### Comments ####### /comments/{id}: parameters: @@ -1426,6 +1462,26 @@ components: organizationId: type: string nullable: true + email: + type: string + nullable: true + emailOnFormCreated: + type: boolean + default: true + emailOnFormSubmitted: + type: boolean + default: true + + UpdateEmailPreferencesDto: + type: object + properties: + email: + type: string + nullable: true + emailOnFormCreated: + type: boolean + emailOnFormSubmitted: + type: boolean UserStatus: type: string diff --git a/deployment/.env.example b/deployment/.env.example index c355fed..e30579a 100755 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -1,3 +1,5 @@ +# Same file as .env in Synology docker directory + # Database Configuration LEGALCONSENTHUB_POSTGRES_USER=legalconsenthub LEGALCONSENTHUB_POSTGRES_PASSWORD=legalconsenthub @@ -8,6 +10,8 @@ KEYCLOAK_POSTGRES_PASSWORD=keycloak KEYCLOAK_POSTGRES_DB=keycloak KEYCLOAK_ISSUER_URL=http://keycloak.lugnas.de +MAIL_HOST=maildev + # Keycloak Configuration KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD= diff --git a/deployment/docker-compose-dev.yaml b/deployment/docker-compose-dev.yaml index 7fb80fa..ebe4ebf 100644 --- a/deployment/docker-compose-dev.yaml +++ b/deployment/docker-compose-dev.yaml @@ -61,3 +61,12 @@ services: retries: 5 start_period: 30s timeout: 10s + + maildev: + image: maildev/maildev:2.2.1 + container_name: legalconsenthub-maildev + ports: + - "1080:1080" + - "1025:1025" + networks: + - legalconsenthub-net diff --git a/deployment/docker-compose-prod.yaml b/deployment/docker-compose-prod.yaml index 5d73b73..d591c95 100755 --- a/deployment/docker-compose-prod.yaml +++ b/deployment/docker-compose-prod.yaml @@ -127,3 +127,12 @@ services: retries: 5 start_period: 30s timeout: 10s + + maildev: + image: maildev/maildev:2.2.1 + container_name: legalconsenthub-maildev + ports: + - "1080:1080" + - "1025:1025" + networks: + - legalconsenthub-net diff --git a/legalconsenthub-backend/build.gradle b/legalconsenthub-backend/build.gradle index 8417be4..9b5ac59 100644 --- a/legalconsenthub-backend/build.gradle +++ b/legalconsenthub-backend/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-mail' implementation "com.openhtmltopdf:openhtmltopdf-core:$openHtmlVersion" implementation "com.openhtmltopdf:openhtmltopdf-pdfbox:$openHtmlVersion" implementation "com.openhtmltopdf:openhtmltopdf-java2d:$openHtmlVersion" diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/LegalconsenthubApplication.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/LegalconsenthubApplication.kt index 0830a8b..7f2fcb1 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/LegalconsenthubApplication.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/LegalconsenthubApplication.kt @@ -3,9 +3,11 @@ package com.betriebsratkanzlei.legalconsenthub import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.scheduling.annotation.EnableAsync @SpringBootApplication @EnableJpaAuditing +@EnableAsync class LegalconsenthubApplication fun main(args: Array) { 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 2bc945f..1c3246d 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 @@ -1,6 +1,8 @@ package com.betriebsratkanzlei.legalconsenthub.application_form import com.betriebsratkanzlei.legalconsenthub.application_form_version.ApplicationFormVersionService +import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormCreatedEvent +import com.betriebsratkanzlei.legalconsenthub.email.ApplicationFormSubmittedEvent import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormInvalidStateException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotCreatedException import com.betriebsratkanzlei.legalconsenthub.error.ApplicationFormNotDeletedException @@ -15,6 +17,7 @@ import com.betriebsratkanzlei.legalconsenthub_api.model.CreateApplicationFormDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateFormElementDto import com.betriebsratkanzlei.legalconsenthub_api.model.CreateNotificationDto import com.betriebsratkanzlei.legalconsenthub_api.model.NotificationType +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service @@ -28,6 +31,7 @@ class ApplicationFormService( private val notificationService: NotificationService, private val versionService: ApplicationFormVersionService, private val userService: UserService, + private val eventPublisher: ApplicationEventPublisher, ) { fun createApplicationForm(createApplicationFormDto: CreateApplicationFormDto): ApplicationForm { val applicationForm = applicationFormMapper.toApplicationForm(createApplicationFormDto) @@ -41,6 +45,15 @@ class ApplicationFormService( val currentUser = userService.getCurrentUser() versionService.createVersion(savedApplicationForm, currentUser) + eventPublisher.publishEvent( + ApplicationFormCreatedEvent( + applicationFormId = savedApplicationForm.id!!, + organizationId = savedApplicationForm.organizationId, + creatorName = currentUser.name, + formName = savedApplicationForm.name, + ), + ) + return savedApplicationForm } @@ -111,6 +124,15 @@ class ApplicationFormService( createNotificationForOrganization(savedApplicationForm) + eventPublisher.publishEvent( + ApplicationFormSubmittedEvent( + applicationFormId = savedApplicationForm.id!!, + organizationId = savedApplicationForm.organizationId, + creatorName = currentUser.name, + formName = savedApplicationForm.name, + ), + ) + return savedApplicationForm } diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormCreatedEvent.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormCreatedEvent.kt new file mode 100644 index 0000000..0998785 --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormCreatedEvent.kt @@ -0,0 +1,10 @@ +package com.betriebsratkanzlei.legalconsenthub.email + +import java.util.UUID + +data class ApplicationFormCreatedEvent( + val applicationFormId: UUID, + val organizationId: String?, + val creatorName: String, + val formName: String, +) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormSubmittedEvent.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormSubmittedEvent.kt new file mode 100644 index 0000000..e7eaf60 --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/ApplicationFormSubmittedEvent.kt @@ -0,0 +1,10 @@ +package com.betriebsratkanzlei.legalconsenthub.email + +import java.util.UUID + +data class ApplicationFormSubmittedEvent( + val applicationFormId: UUID, + val organizationId: String?, + val creatorName: String, + val formName: String, +) diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailEventListener.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailEventListener.kt new file mode 100644 index 0000000..c79604f --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailEventListener.kt @@ -0,0 +1,73 @@ +package com.betriebsratkanzlei.legalconsenthub.email + +import com.betriebsratkanzlei.legalconsenthub.user.UserRepository +import org.slf4j.LoggerFactory +import org.springframework.context.event.EventListener +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component + +@Component +class EmailEventListener( + private val userRepository: UserRepository, + private val emailService: EmailService, +) { + private val logger = LoggerFactory.getLogger(EmailEventListener::class.java) + + @Async + @EventListener + fun handleApplicationFormCreated(event: ApplicationFormCreatedEvent) { + logger.info("Processing ApplicationFormCreatedEvent for form: ${event.formName}") + + val recipients = + userRepository + .findByOrganizationIdAndEmailOnFormCreatedTrue(event.organizationId) + .filter { !it.email.isNullOrBlank() } + + logger.info("Found ${recipients.size} recipients for form created event") + + recipients.forEach { user -> + val subject = "Neuer Mitbestimmungsantrag: ${event.formName}" + val body = + emailService.buildFormCreatedEmail( + formName = event.formName, + creatorName = event.creatorName, + applicationFormId = event.applicationFormId, + ) + + emailService.sendEmail( + to = user.email!!, + subject = subject, + body = body, + ) + } + } + + @Async + @EventListener + fun handleApplicationFormSubmitted(event: ApplicationFormSubmittedEvent) { + logger.info("Processing ApplicationFormSubmittedEvent for form: ${event.formName}") + + val recipients = + userRepository + .findByOrganizationIdAndEmailOnFormSubmittedTrue(event.organizationId) + .filter { !it.email.isNullOrBlank() } + + logger.info("Found ${recipients.size} recipients for form submitted event") + + recipients.forEach { user -> + val subject = "Mitbestimmungsantrag eingereicht: ${event.formName}" + val body = + emailService.buildFormSubmittedEmail( + formName = event.formName, + creatorName = event.creatorName, + applicationFormId = event.applicationFormId, + ) + + emailService.sendEmail( + to = user.email!!, + subject = subject, + body = body, + ) + } + } +} diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailService.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailService.kt new file mode 100644 index 0000000..6471a9f --- /dev/null +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/email/EmailService.kt @@ -0,0 +1,63 @@ +package com.betriebsratkanzlei.legalconsenthub.email + +import org.slf4j.LoggerFactory +import org.springframework.mail.MailException +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.MimeMessageHelper +import org.springframework.stereotype.Service +import org.thymeleaf.TemplateEngine +import org.thymeleaf.context.Context +import java.util.UUID + +@Service +class EmailService( + private val mailSender: JavaMailSender, + private val templateEngine: TemplateEngine, +) { + private val logger = LoggerFactory.getLogger(EmailService::class.java) + + fun sendEmail( + to: String, + subject: String, + body: String, + ) { + try { + val message = mailSender.createMimeMessage() + val helper = MimeMessageHelper(message, true, "UTF-8") + + helper.setTo(to) + helper.setSubject(subject) + helper.setText(body, true) + helper.setFrom("noreply@legalconsenthub.com") + + mailSender.send(message) + logger.info("Email sent successfully to: $to") + } catch (e: MailException) { + logger.error("Failed to send email to: $to", e) + } + } + + fun buildFormCreatedEmail( + formName: String, + creatorName: String, + applicationFormId: UUID, + ): String { + val context = Context() + context.setVariable("formName", formName) + context.setVariable("creatorName", creatorName) + context.setVariable("applicationFormId", applicationFormId) + return templateEngine.process("email/form_created", context) + } + + fun buildFormSubmittedEmail( + formName: String, + creatorName: String, + applicationFormId: UUID, + ): String { + val context = Context() + context.setVariable("formName", formName) + context.setVariable("creatorName", creatorName) + context.setVariable("applicationFormId", applicationFormId) + return templateEngine.process("email/form_submitted", context) + } +} 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 index 2de94e0..895a4dd 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/security/JwtUserSyncFilter.kt @@ -34,6 +34,7 @@ class JwtUserSyncFilter( val jwt: Jwt = auth.token val keycloakId = jwt.subject val name = jwt.getClaimAsString("name") + val email = jwt.getClaimAsString("email") // Extract organization information from JWT val organizationClaim = jwt.getClaimAsMap("organization") @@ -46,7 +47,15 @@ class JwtUserSyncFilter( } } - val user = UserDto(keycloakId, name, organizationId) + val user = + UserDto( + keycloakId = keycloakId, + name = name, + organizationId = organizationId, + email = email, + emailOnFormCreated = null, + emailOnFormSubmitted = null, + ) if (keycloakId != null) { userService.createUpdateUserFromJwt(user) 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 d94223e..6dc461a 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 @@ -21,6 +21,12 @@ class User( var name: String, @Column(nullable = true) var organizationId: String? = null, + @Column(nullable = true) + var email: String? = null, + @Column(nullable = false) + var emailOnFormCreated: Boolean = true, + @Column(nullable = false) + var emailOnFormSubmitted: Boolean = true, @CreatedDate @Column(nullable = false) var createdAt: LocalDateTime? = null, 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 731b7cf..3413763 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,6 +1,7 @@ package com.betriebsratkanzlei.legalconsenthub.user import com.betriebsratkanzlei.legalconsenthub_api.api.UserApi +import com.betriebsratkanzlei.legalconsenthub_api.model.UpdateEmailPreferencesDto import com.betriebsratkanzlei.legalconsenthub_api.model.UserDto import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RestController @@ -19,4 +20,18 @@ class UserController( userService.deleteUser(id) return ResponseEntity.noContent().build() } + + override fun updateUserEmailPreferences( + id: String, + updateEmailPreferencesDto: UpdateEmailPreferencesDto, + ): ResponseEntity { + val user = + userService.updateEmailPreferences( + userId = id, + email = updateEmailPreferencesDto.email, + emailOnFormCreated = updateEmailPreferencesDto.emailOnFormCreated ?: true, + emailOnFormSubmitted = updateEmailPreferencesDto.emailOnFormSubmitted ?: true, + ) + return ResponseEntity.ok(userMapper.toUserDto(user)) + } } 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 095a714..268ca3d 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 @@ -10,6 +10,9 @@ class UserMapper { keycloakId = user.keycloakId, name = user.name, organizationId = user.organizationId, + email = user.email, + emailOnFormCreated = user.emailOnFormCreated, + emailOnFormSubmitted = user.emailOnFormSubmitted, ) fun toUser(userDto: UserDto): User { @@ -18,6 +21,9 @@ class UserMapper { keycloakId = userDto.keycloakId, name = userDto.name, organizationId = userDto.organizationId, + email = userDto.email, + emailOnFormCreated = userDto.emailOnFormCreated ?: true, + emailOnFormSubmitted = userDto.emailOnFormSubmitted ?: true, ) return user diff --git a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRepository.kt b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRepository.kt index 79477fd..e8e3796 100644 --- a/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRepository.kt +++ b/legalconsenthub-backend/src/main/kotlin/com/betriebsratkanzlei/legalconsenthub/user/UserRepository.kt @@ -4,4 +4,8 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface UserRepository : JpaRepository +interface UserRepository : JpaRepository { + fun findByOrganizationIdAndEmailOnFormCreatedTrue(organizationId: String?): List + + fun findByOrganizationIdAndEmailOnFormSubmittedTrue(organizationId: String?): List +} 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 2827b4a..08a3d0f 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 @@ -17,9 +17,7 @@ class UserService( val principal = SecurityContextHolder.getContext().authentication.principal as CustomJwtTokenPrincipal val userId = principal.id ?: throw IllegalStateException("User ID not found") - return userRepository - .findById(userId) - .orElseThrow { UserNotFoundException(userId) } + return userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) } } @Transactional @@ -29,11 +27,7 @@ class UserService( 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)) + return updateUser(userDto) } } @@ -47,33 +41,49 @@ class UserService( keycloakId = userDto.keycloakId, name = userDto.name, organizationId = userDto.organizationId, + email = userDto.email, ) return userRepository.save(user) } fun getUserById(userId: String): User = - userRepository - .findById(userId) - .orElseThrow { UserNotFoundException(userId) } + userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) } @Transactional fun updateUser(userDto: UserDto): User { - val user = - userRepository - .findById(userDto.keycloakId) - .orElseThrow { UserNotFoundException(userDto.keycloakId) } + val user = userRepository.findById(userDto.keycloakId).orElseThrow { UserNotFoundException(userDto.keycloakId) } user.name = userDto.name - // Only update organization if it's not already set + if (user.organizationId == null && userDto.organizationId != null) { user.organizationId = userDto.organizationId } + if (userDto.email != null && user.email != userDto.email) { + user.email = userDto.email + } + return userRepository.save(user) } fun deleteUser(userId: String) { userRepository.deleteById(userId) } + + @Transactional + fun updateEmailPreferences( + userId: String, + email: String?, + emailOnFormCreated: Boolean, + emailOnFormSubmitted: Boolean, + ): User { + val user = userRepository.findById(userId).orElseThrow { UserNotFoundException(userId) } + + user.email = email + user.emailOnFormCreated = emailOnFormCreated + user.emailOnFormSubmitted = emailOnFormSubmitted + + return userRepository.save(user) + } } diff --git a/legalconsenthub-backend/src/main/resources/application.yaml b/legalconsenthub-backend/src/main/resources/application.yaml index 074dd6b..7a48d48 100644 --- a/legalconsenthub-backend/src/main/resources/application.yaml +++ b/legalconsenthub-backend/src/main/resources/application.yaml @@ -38,3 +38,21 @@ spring: jwt: issuer-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI} jwk-set-uri: ${SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI} + + mail: + host: ${MAIL_HOST:0.0.0.0} + port: ${MAIL_PORT:1025} + username: ${MAIL_USERNAME:} + password: ${MAIL_PASSWORD:} + properties: + mail: + smtp: + auth: false + starttls: + enable: false + required: false + ssl: + enable: false + connectiontimeout: 5000 + timeout: 5000 + writetimeout: 5000 diff --git a/legalconsenthub-backend/src/main/resources/templates/email/form_created.html b/legalconsenthub-backend/src/main/resources/templates/email/form_created.html new file mode 100644 index 0000000..16bad5a --- /dev/null +++ b/legalconsenthub-backend/src/main/resources/templates/email/form_created.html @@ -0,0 +1,35 @@ + + + + + + + +
+
+

Neuer Mitbestimmungsantrag erstellt

+
+
+

Hallo,

+

Ein neuer Mitbestimmungsantrag wurde erstellt:

+
    +
  • Antrag:
  • +
  • Erstellt von:
  • +
+

Klicken Sie auf den folgenden Link, um den Antrag anzusehen:

+ Antrag ansehen +
+ +
+ + + diff --git a/legalconsenthub-backend/src/main/resources/templates/email/form_submitted.html b/legalconsenthub-backend/src/main/resources/templates/email/form_submitted.html new file mode 100644 index 0000000..c95ee3a --- /dev/null +++ b/legalconsenthub-backend/src/main/resources/templates/email/form_submitted.html @@ -0,0 +1,35 @@ + + + + + + + +
+
+

Mitbestimmungsantrag eingereicht

+
+
+

Hallo,

+

Ein Mitbestimmungsantrag wurde zur Prüfung eingereicht:

+
    +
  • Antrag:
  • +
  • Eingereicht von:
  • +
+

Klicken Sie auf den folgenden Link, um den Antrag zu prüfen:

+ Antrag prüfen +
+ +
+ + + diff --git a/legalconsenthub/app/composables/index.ts b/legalconsenthub/app/composables/index.ts index f063728..fc25c02 100644 --- a/legalconsenthub/app/composables/index.ts +++ b/legalconsenthub/app/composables/index.ts @@ -5,3 +5,5 @@ export { useApplicationFormVersionApi } from './applicationFormVersion/useApplic export { useApplicationFormNavigation } from './useApplicationFormNavigation' export { useNotification } from './notification/useNotification' export { useNotificationApi } from './notification/useNotificationApi' +export { useUser } from './user/useUser' +export { useUserApi } from './user/useUserApi' diff --git a/legalconsenthub/app/composables/user/useUser.ts b/legalconsenthub/app/composables/user/useUser.ts new file mode 100644 index 0000000..1d74b6f --- /dev/null +++ b/legalconsenthub/app/composables/user/useUser.ts @@ -0,0 +1,26 @@ +import type { UpdateEmailPreferencesDto, UserDto } from '~~/.api-client' +import { useUserApi } from '~/composables' + +export function useUser() { + const userApi = useUserApi() + + async function getUserById(userId: string): Promise { + return await userApi.getUserById(userId) + } + + async function updateEmailPreferences( + userId: string, + email: string | null, + emailOnFormCreated: boolean, + emailOnFormSubmitted: boolean + ): Promise { + const updateDto: UpdateEmailPreferencesDto = { email, emailOnFormCreated, emailOnFormSubmitted } + + return await userApi.updateEmailPreferences(userId, updateDto) + } + + return { + getUserById, + updateEmailPreferences + } +} diff --git a/legalconsenthub/app/composables/user/useUserApi.ts b/legalconsenthub/app/composables/user/useUserApi.ts new file mode 100644 index 0000000..9040816 --- /dev/null +++ b/legalconsenthub/app/composables/user/useUserApi.ts @@ -0,0 +1,42 @@ +import { UserApi, Configuration, type UserDto, type UpdateEmailPreferencesDto } from '~~/.api-client' +import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo' +import { wrappedFetchWrap } from '~/utils/wrappedFetch' + +export function useUserApi() { + const appBaseUrl = useRuntimeConfig().app.baseURL + const { serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public + + const basePath = withoutTrailingSlash( + cleanDoubleSlashes( + import.meta.client + ? appBaseUrl + clientProxyBasePath + : useRequestURL().origin + clientProxyBasePath + serverApiBasePath + ) + ) + + const userApiClient = new UserApi( + new Configuration({ basePath, fetchApi: wrappedFetchWrap(useRequestFetch()) }) + ) + + async function getUserById(id: string): Promise { + return userApiClient.getUserById({ id }) + } + + async function updateEmailPreferences( + id: string, + updateEmailPreferencesDto: UpdateEmailPreferencesDto + ): Promise { + return userApiClient.updateUserEmailPreferences({ id, updateEmailPreferencesDto }) + } + + async function deleteUser(id: string): Promise { + return userApiClient.deleteUser({ id }) + } + + return { + getUserById, + updateEmailPreferences, + deleteUser + } +} + diff --git a/legalconsenthub/app/pages/settings.vue b/legalconsenthub/app/pages/settings.vue index 8976497..36ede73 100644 --- a/legalconsenthub/app/pages/settings.vue +++ b/legalconsenthub/app/pages/settings.vue @@ -72,6 +72,33 @@ + + + + + +
+ + +
+ + +
+ + +
+
@@ -79,6 +106,8 @@