From 63fa3b8616ac58651080eaa7f0605dc14bcf17ba Mon Sep 17 00:00:00 2001 From: NathanFallet Date: Fri, 12 Apr 2024 02:31:36 +0200 Subject: [PATCH] feat: contact form (get in touch) sending mail + discord webhook --- .../controllers/web/IWebController.kt | 6 ++++ .../controllers/web/WebController.kt | 7 ++++ .../groupeminaste/plugins/Koin.kt | 24 ++++++++++++- .../services/discord/DiscordService.kt | 31 +++++++++++++++++ .../services/discord/IDiscordService.kt | 9 +++++ .../services/email/EmailsService.kt | 34 +++++++++++++++++++ .../services/email/IEmailsService.kt | 9 +++++ .../usecases/application/GetInTouchUseCase.kt | 32 +++++++++++++++++ .../application/IGetInTouchUseCase.kt | 6 ++++ .../application/ISendDiscordWebhookUseCase.kt | 6 ++++ .../application/SendDiscordWebhookUseCase.kt | 13 +++++++ .../usecases/application/SendEmailUseCase.kt | 15 ++++++++ .../src/commonMain/resources/application.conf | 10 ++++++ .../resources/application.test.conf | 8 +++++ .../resources/i18n/Messages_en.properties | 2 ++ .../templates/components/sections.ftl | 12 +++---- .../resources/templates/public/getintouch.ftl | 21 ++++++++++++ .../models/application/DiscordWebhook.kt | 7 ++++ .../groupeminaste/models/application/Email.kt | 8 +++++ .../models/application/GetInTouchPayload.kt | 10 ++++++ 20 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/DiscordService.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/IDiscordService.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/EmailsService.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/IEmailsService.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/GetInTouchUseCase.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/IGetInTouchUseCase.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/ISendDiscordWebhookUseCase.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendDiscordWebhookUseCase.kt create mode 100644 groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendEmailUseCase.kt create mode 100644 groupeminaste-backend/src/commonMain/resources/templates/public/getintouch.ftl create mode 100644 groupeminaste-commons/src/commonMain/kotlin/me/nathanfallet/groupeminaste/models/application/DiscordWebhook.kt create mode 100644 groupeminaste-commons/src/commonMain/kotlin/me/nathanfallet/groupeminaste/models/application/Email.kt create mode 100644 groupeminaste-commons/src/commonMain/kotlin/me/nathanfallet/groupeminaste/models/application/GetInTouchPayload.kt diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/IWebController.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/IWebController.kt index 4063373..61a5598 100644 --- a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/IWebController.kt +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/IWebController.kt @@ -1,7 +1,9 @@ package me.nathanfallet.groupeminaste.controllers.web +import me.nathanfallet.groupeminaste.models.application.GetInTouchPayload import me.nathanfallet.ktorx.controllers.IUnitController import me.nathanfallet.ktorx.models.annotations.Path +import me.nathanfallet.ktorx.models.annotations.Payload import me.nathanfallet.ktorx.models.annotations.TemplateMapping import me.nathanfallet.ktorx.models.responses.RedirectResponse @@ -15,4 +17,8 @@ interface IWebController : IUnitController { @Path("GET", "/discord") fun discord(): RedirectResponse + @TemplateMapping("public/getintouch.ftl") + @Path("POST", "/getintouch") + suspend fun getInTouch(@Payload payload: GetInTouchPayload) + } diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/WebController.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/WebController.kt index a8a88f3..958ebac 100644 --- a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/WebController.kt +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/controllers/web/WebController.kt @@ -1,13 +1,16 @@ package me.nathanfallet.groupeminaste.controllers.web +import me.nathanfallet.groupeminaste.models.application.GetInTouchPayload import me.nathanfallet.groupeminaste.models.projects.Project import me.nathanfallet.groupeminaste.models.users.User +import me.nathanfallet.groupeminaste.usecases.application.IGetInTouchUseCase import me.nathanfallet.ktorx.models.responses.RedirectResponse import me.nathanfallet.usecases.models.list.IListModelSuspendUseCase class WebController( private val listUsersUseCase: IListModelSuspendUseCase, private val listProjectsUseCase: IListModelSuspendUseCase, + private val getInTouchUseCase: IGetInTouchUseCase, ) : IWebController { override suspend fun home(): Map { @@ -23,4 +26,8 @@ class WebController( "https://discord.gg/PeTpuCWnqs", true ) + override suspend fun getInTouch(payload: GetInTouchPayload) { + getInTouchUseCase(payload) + } + } diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/plugins/Koin.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/plugins/Koin.kt index 6684981..4f9be19 100644 --- a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/plugins/Koin.kt +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/plugins/Koin.kt @@ -23,8 +23,13 @@ import me.nathanfallet.groupeminaste.models.users.User import me.nathanfallet.groupeminaste.repositories.projects.IProjectLinksRepository import me.nathanfallet.groupeminaste.repositories.projects.IProjectsRepository import me.nathanfallet.groupeminaste.repositories.users.IUsersRepository +import me.nathanfallet.groupeminaste.services.discord.DiscordService +import me.nathanfallet.groupeminaste.services.discord.IDiscordService +import me.nathanfallet.groupeminaste.services.email.EmailsService +import me.nathanfallet.groupeminaste.services.email.IEmailsService import me.nathanfallet.groupeminaste.services.jwt.IJWTService import me.nathanfallet.groupeminaste.services.jwt.JWTService +import me.nathanfallet.groupeminaste.usecases.application.* import me.nathanfallet.groupeminaste.usecases.auth.* import me.nathanfallet.groupeminaste.usecases.users.GetUserForCallUseCase import me.nathanfallet.groupeminaste.usecases.web.GetAdminMenuForCallUseCase @@ -38,6 +43,7 @@ import me.nathanfallet.ktorx.usecases.users.IGetUserForCallUseCase import me.nathanfallet.ktorx.usecases.users.IRequireUserForCallUseCase import me.nathanfallet.ktorx.usecases.users.RequireUserForCallUseCase import me.nathanfallet.surexposed.database.IDatabase +import me.nathanfallet.usecases.emails.ISendEmailUseCase import me.nathanfallet.usecases.localization.ITranslateUseCase import me.nathanfallet.usecases.models.create.CreateChildModelFromRepositorySuspendUseCase import me.nathanfallet.usecases.models.create.CreateModelFromRepositorySuspendUseCase @@ -77,12 +83,24 @@ fun Application.configureKoin() { } } val serviceModule = module { + single { + EmailsService( + environment.config.property("email.host").getString(), + environment.config.property("email.username").getString(), + environment.config.property("email.password").getString() + ) + } single { JWTService( environment.config.property("jwt.secret").getString(), environment.config.property("jwt.issuer").getString() ) } + single { + DiscordService( + environment.config.property("discord.getInTouch").getString() + ) + } } val repositoryModule = module { // Application @@ -97,8 +115,11 @@ fun Application.configureKoin() { } val useCaseModule = module { // Application + single { SendEmailUseCase(get()) } + single { SendDiscordWebhookUseCase(get()) } single { TranslateUseCase() } single { GetLocaleForCallUseCase() } + single { GetInTouchUseCase(get(), get()) } // Auth single { HashPasswordUseCase() } @@ -159,7 +180,8 @@ fun Application.configureKoin() { single { WebController( get(named()), - get(named()) + get(named()), + get() ) } single { DashboardController() } diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/DiscordService.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/DiscordService.kt new file mode 100644 index 0000000..19d8d40 --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/DiscordService.kt @@ -0,0 +1,31 @@ +package me.nathanfallet.groupeminaste.services.discord + +import io.ktor.client.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import me.nathanfallet.groupeminaste.models.application.DiscordWebhook +import me.nathanfallet.groupeminaste.models.application.GroupeMinasteJson + +class DiscordService( + private val getInTouchWebhook: String, +) : IDiscordService { + + private val client = HttpClient { + install(ContentNegotiation) { + json(GroupeMinasteJson.json) + } + } + + override suspend fun sendWebhookMessage(webhook: DiscordWebhook, message: String) { + val url = when (webhook) { + DiscordWebhook.GET_IN_TOUCH -> getInTouchWebhook + } + client.post(url) { + contentType(ContentType.Application.Json) + setBody(mapOf("content" to message)) + } + } + +} diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/IDiscordService.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/IDiscordService.kt new file mode 100644 index 0000000..9167faa --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/discord/IDiscordService.kt @@ -0,0 +1,9 @@ +package me.nathanfallet.groupeminaste.services.discord + +import me.nathanfallet.groupeminaste.models.application.DiscordWebhook + +interface IDiscordService { + + suspend fun sendWebhookMessage(webhook: DiscordWebhook, message: String) + +} diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/EmailsService.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/EmailsService.kt new file mode 100644 index 0000000..ca2612a --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/EmailsService.kt @@ -0,0 +1,34 @@ +package me.nathanfallet.groupeminaste.services.email + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import me.nathanfallet.groupeminaste.models.application.Email +import me.nathanfallet.usecases.emails.IEmail +import org.apache.commons.mail.HtmlEmail + +class EmailsService( + private val host: String, + private val username: String, + private val password: String, +) : IEmailsService { + + override fun sendEmail(email: IEmail, destination: List) { + if (email !is Email) return + CoroutineScope(Job()).launch { + destination.forEach { target -> + val htmlEmail = HtmlEmail() + htmlEmail.hostName = host + htmlEmail.isStartTLSEnabled = true + htmlEmail.setSmtpPort(587) + htmlEmail.setAuthentication(username, password) + htmlEmail.setFrom(username) + htmlEmail.addTo(target) + htmlEmail.subject = email.title + htmlEmail.setHtmlMsg(email.body) + htmlEmail.send() + } + } + } + +} diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/IEmailsService.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/IEmailsService.kt new file mode 100644 index 0000000..48c98cf --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/services/email/IEmailsService.kt @@ -0,0 +1,9 @@ +package me.nathanfallet.groupeminaste.services.email + +import me.nathanfallet.usecases.emails.IEmail + +interface IEmailsService { + + fun sendEmail(email: IEmail, destination: List) + +} diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/GetInTouchUseCase.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/GetInTouchUseCase.kt new file mode 100644 index 0000000..24b8103 --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/GetInTouchUseCase.kt @@ -0,0 +1,32 @@ +package me.nathanfallet.groupeminaste.usecases.application + +import me.nathanfallet.groupeminaste.models.application.DiscordWebhook +import me.nathanfallet.groupeminaste.models.application.Email +import me.nathanfallet.groupeminaste.models.application.GetInTouchPayload +import me.nathanfallet.usecases.emails.ISendEmailUseCase + +class GetInTouchUseCase( + private val sendEmailUseCase: ISendEmailUseCase, + private val sendDiscordWebhookUseCase: ISendDiscordWebhookUseCase, +) : IGetInTouchUseCase { + + override suspend fun invoke(input: GetInTouchPayload) { + val message = """ + |Nom: ${input.name} + |Email: ${input.email} + | + |Message: + |${input.message} + """.trimMargin() + + sendEmailUseCase( + Email("Have an Project in Mind?", message), + listOf("contact@groupe-minaste.org") + ) + sendDiscordWebhookUseCase( + DiscordWebhook.GET_IN_TOUCH, + message + ) + } + +} diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/IGetInTouchUseCase.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/IGetInTouchUseCase.kt new file mode 100644 index 0000000..4bdceed --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/IGetInTouchUseCase.kt @@ -0,0 +1,6 @@ +package me.nathanfallet.groupeminaste.usecases.application + +import me.nathanfallet.groupeminaste.models.application.GetInTouchPayload +import me.nathanfallet.usecases.base.ISuspendUseCase + +interface IGetInTouchUseCase : ISuspendUseCase diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/ISendDiscordWebhookUseCase.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/ISendDiscordWebhookUseCase.kt new file mode 100644 index 0000000..cfbf5f3 --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/ISendDiscordWebhookUseCase.kt @@ -0,0 +1,6 @@ +package me.nathanfallet.groupeminaste.usecases.application + +import me.nathanfallet.groupeminaste.models.application.DiscordWebhook +import me.nathanfallet.usecases.base.IPairSuspendUseCase + +interface ISendDiscordWebhookUseCase : IPairSuspendUseCase diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendDiscordWebhookUseCase.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendDiscordWebhookUseCase.kt new file mode 100644 index 0000000..6384a2c --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendDiscordWebhookUseCase.kt @@ -0,0 +1,13 @@ +package me.nathanfallet.groupeminaste.usecases.application + +import me.nathanfallet.groupeminaste.models.application.DiscordWebhook +import me.nathanfallet.groupeminaste.services.discord.IDiscordService + +class SendDiscordWebhookUseCase( + private val discordService: IDiscordService, +) : ISendDiscordWebhookUseCase { + + override suspend fun invoke(input1: DiscordWebhook, input2: String) = + discordService.sendWebhookMessage(input1, input2) + +} diff --git a/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendEmailUseCase.kt b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendEmailUseCase.kt new file mode 100644 index 0000000..7d7c6c4 --- /dev/null +++ b/groupeminaste-backend/src/commonMain/kotlin/me/nathanfallet/groupeminaste/usecases/application/SendEmailUseCase.kt @@ -0,0 +1,15 @@ +package me.nathanfallet.groupeminaste.usecases.application + +import me.nathanfallet.groupeminaste.services.email.IEmailsService +import me.nathanfallet.usecases.emails.IEmail +import me.nathanfallet.usecases.emails.ISendEmailUseCase + +class SendEmailUseCase( + private val emailsService: IEmailsService, +) : ISendEmailUseCase { + + override fun invoke(input1: IEmail, input2: List) { + emailsService.sendEmail(input1, input2) + } + +} diff --git a/groupeminaste-backend/src/commonMain/resources/application.conf b/groupeminaste-backend/src/commonMain/resources/application.conf index b2c3184..012a325 100644 --- a/groupeminaste-backend/src/commonMain/resources/application.conf +++ b/groupeminaste-backend/src/commonMain/resources/application.conf @@ -25,3 +25,13 @@ jwt { secret = ${?JWT_SECRET} issuer = "groupeminaste" } +email { + host = "mail.groupe-minaste.org" + username = "contact@groupe-minaste.org" + password = "" + password = ${?EMAIL_PASSWORD} +} +discord { + getInTouch = "" + getInTouch = ${?DISCORD_GET_IN_TOUCH} +} diff --git a/groupeminaste-backend/src/commonMain/resources/application.test.conf b/groupeminaste-backend/src/commonMain/resources/application.test.conf index 332e096..3b8cbb4 100644 --- a/groupeminaste-backend/src/commonMain/resources/application.test.conf +++ b/groupeminaste-backend/src/commonMain/resources/application.test.conf @@ -19,3 +19,11 @@ jwt { secret = "test" issuer = "groupeminaste" } +email { + host = "mail.groupe-minaste.org" + username = "contact@groupe-minaste.org" + password = "" +} +discord { + getInTouch = "" +} diff --git a/groupeminaste-backend/src/commonMain/resources/i18n/Messages_en.properties b/groupeminaste-backend/src/commonMain/resources/i18n/Messages_en.properties index befb4e5..1cd7bb4 100644 --- a/groupeminaste-backend/src/commonMain/resources/i18n/Messages_en.properties +++ b/groupeminaste-backend/src/commonMain/resources/i18n/Messages_en.properties @@ -59,3 +59,5 @@ admin_links_id=Id admin_links_name=Name admin_links_url=URL admin_links_view=View links for this project +home_contact_confirmation=Thank you for your message! We will get back to you as soon as possible. +home_contact_back=Go back to home diff --git a/groupeminaste-backend/src/commonMain/resources/templates/components/sections.ftl b/groupeminaste-backend/src/commonMain/resources/templates/components/sections.ftl index 7ecd5dd..f38c468 100644 --- a/groupeminaste-backend/src/commonMain/resources/templates/components/sections.ftl +++ b/groupeminaste-backend/src/commonMain/resources/templates/components/sections.ftl @@ -32,9 +32,7 @@
image -