Skip to content

Commit

Permalink
feat: contact form (get in touch) sending mail + discord webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Apr 12, 2024
1 parent 03afade commit 63fa3b8
Show file tree
Hide file tree
Showing 20 changed files with 262 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)

}
Original file line number Diff line number Diff line change
@@ -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<User>,
private val listProjectsUseCase: IListModelSuspendUseCase<Project>,
private val getInTouchUseCase: IGetInTouchUseCase,
) : IWebController {

override suspend fun home(): Map<String, Any> {
Expand All @@ -23,4 +26,8 @@ class WebController(
"https://discord.gg/PeTpuCWnqs", true
)

override suspend fun getInTouch(payload: GetInTouchPayload) {
getInTouchUseCase(payload)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -77,12 +83,24 @@ fun Application.configureKoin() {
}
}
val serviceModule = module {
single<IEmailsService> {
EmailsService(
environment.config.property("email.host").getString(),
environment.config.property("email.username").getString(),
environment.config.property("email.password").getString()
)
}
single<IJWTService> {
JWTService(
environment.config.property("jwt.secret").getString(),
environment.config.property("jwt.issuer").getString()
)
}
single<IDiscordService> {
DiscordService(
environment.config.property("discord.getInTouch").getString()
)
}
}
val repositoryModule = module {
// Application
Expand All @@ -97,8 +115,11 @@ fun Application.configureKoin() {
}
val useCaseModule = module {
// Application
single<ISendEmailUseCase> { SendEmailUseCase(get()) }
single<ISendDiscordWebhookUseCase> { SendDiscordWebhookUseCase(get()) }
single<ITranslateUseCase> { TranslateUseCase() }
single<IGetLocaleForCallUseCase> { GetLocaleForCallUseCase() }
single<IGetInTouchUseCase> { GetInTouchUseCase(get(), get()) }

// Auth
single<IHashPasswordUseCase> { HashPasswordUseCase() }
Expand Down Expand Up @@ -159,7 +180,8 @@ fun Application.configureKoin() {
single<IWebController> {
WebController(
get(named<User>()),
get(named<Project>())
get(named<Project>()),
get()
)
}
single<IDashboardController> { DashboardController() }
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}

}
Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
@@ -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<String>) {
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()
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package me.nathanfallet.groupeminaste.services.email

import me.nathanfallet.usecases.emails.IEmail

interface IEmailsService {

fun sendEmail(email: IEmail, destination: List<String>)

}
Original file line number Diff line number Diff line change
@@ -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
)
}

}
Original file line number Diff line number Diff line change
@@ -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<GetInTouchPayload, Unit>
Original file line number Diff line number Diff line change
@@ -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<DiscordWebhook, String, Unit>
Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
@@ -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<String>) {
emailsService.sendEmail(input1, input2)
}

}
10 changes: 10 additions & 0 deletions groupeminaste-backend/src/commonMain/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ jwt {
secret = "test"
issuer = "groupeminaste"
}
email {
host = "mail.groupe-minaste.org"
username = "contact@groupe-minaste.org"
password = ""
}
discord {
getInTouch = ""
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@
<div class="mb-12">
<div class="relative group mb-8 overflow-hidden shadow-service rounded-md">
<img src="${project.image}" alt="image" class="w-full"/>
<div
class="absolute w-full h-full top-0 left-0 bg-primary bg-opacity-[17%] flex items-center justify-center opacity-0 invisible group-hover:opacity-100 group-hover:visible transition"
>
<div class="absolute w-full h-full top-0 left-0 bg-primary bg-opacity-[17%] flex items-center justify-center opacity-0 invisible group-hover:opacity-100 group-hover:visible transition">
<a href="${project.image}"
class="glightbox w-10 h-10 flex items-center justify-center bg-primary text-white rounded-full">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"
Expand Down Expand Up @@ -185,24 +183,24 @@
</div>
<div class="flex justify-center -mx-4">
<div class="w-full lg:w-9/12 px-4">
<form>
<form method="post" action="/${locale}/getintouch">
<div class="flex flex-wrap -mx-4">
<div class="w-full md:w-1/2 px-4">
<div class="mb-6">
<input type="text" placeholder="<@t key="home_contact_field_name" />"
<input type="text" placeholder="<@t key="home_contact_field_name" />" name="name"
class="input-field"/>
</div>
</div>
<div class="w-full md:w-1/2 px-4">
<div class="mb-6">
<input type="email" placeholder="<@t key="home_contact_field_email" />"
<input type="email" placeholder="<@t key="home_contact_field_email" />" name="email"
class="input-field"/>
</div>
</div>
<div class="w-full px-4">
<div class="mb-6">
<textarea rows="4" placeholder="<@t key="home_contact_field_tell_us" />"
class="input-field resize-none"></textarea>
name="message" class="input-field resize-none"></textarea>
</div>
</div>
<div class="w-full px-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<#import "../template.ftl" as template>
<@template.page>
<section id="contact" class="py-[120px]">
<div class="container">
<div class="flex flex-wrap mx-[-16px]">
<div class="w-full px-4">
<div class="max-w-[600px] mx-auto text-center mb-[50px]">
<span class="font-semibold text-lg text-primary block mb-2"><@t key="home_contact_headline" /></span>
<h2 class="font-bold text-black text-3xl sm:text-4xl md:text-[45px] mb-5"><@t key="home_contact_title" /></h2>
<p class="font-medium text-lg text-body-color"><@t key="home_contact_confirmation" /></p>

<a href="/${locale}"
class="pt-4 inline-flex justify-center items-center py-4 px-9 rounded-full font-semibold text-white bg-primary mx-auto transition duration-300 ease-in-out hover:shadow-signUp hover:bg-opacity-90">
<@t key="home_contact_back" />
</a>
</div>
</div>
</div>
</div>
</section>
</@template.page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.nathanfallet.groupeminaste.models.application

enum class DiscordWebhook {

GET_IN_TOUCH

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package me.nathanfallet.groupeminaste.models.application

import me.nathanfallet.usecases.emails.IEmail

data class Email(
val title: String,
val body: String,
) : IEmail
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.nathanfallet.groupeminaste.models.application

import kotlinx.serialization.Serializable

@Serializable
data class GetInTouchPayload(
val name: String,
val email: String,
val message: String,
)

0 comments on commit 63fa3b8

Please sign in to comment.