Skip to content

Commit

Permalink
[KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (#19)
Browse files Browse the repository at this point in the history
* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 - test

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (패스워드 재설정 기능 추가)

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (패스워드 재설정 기능 추가)

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (네이밍 변경

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (네이밍 변경

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (네이밍 변경

* [KAN-34] 유저 - 이메일인증 메일 전송 API 개발 (네이밍 변경 (ktlint)
  • Loading branch information
sinkyoungdeok authored May 8, 2024
1 parent fa78f1d commit fd532e2
Show file tree
Hide file tree
Showing 16 changed files with 226 additions and 10 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: Test
on:
pull_request:

env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}

jobs:
build-and-test:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ docker/redis/**

### IDEA run ###
/.run
.env

11 changes: 11 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.hibernate.validator:hibernate-validator:6.1.2.Final")

// AWS SES
implementation("software.amazon.awssdk:ses:2.20.114")

// Kotlin
val coroutineVersion = "1.6.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion")
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$coroutineVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutineVersion")

// Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("junit", "junit", "4.13.2")
Expand Down
4 changes: 4 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ services:
- "8081:8081"
environment:
- USE_PROFILE=prod1
env_file:
- .env
restart: always
skku-api-2:
depends_on:
Expand All @@ -26,6 +28,8 @@ services:
- "8082:8082"
environment:
- USE_PROFILE=prod2
env_file:
- .env
restart: always
skku-db:
image: mysql:8.0
Expand Down
23 changes: 23 additions & 0 deletions src/main/kotlin/com/restaurant/be/common/config/AwsSESConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.restaurant.be.common.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.ses.SesClient

@Configuration
class AwsSESConfig(
@Value("\${aws.accessKey}") val accessKey: String,
@Value("\${aws.secretKey}") val secretKey: String
) {
private val awsCred = AwsBasicCredentials.create(accessKey, secretKey)

@Bean
fun sesClient(): SesClient = SesClient.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(StaticCredentialsProvider.create(awsCred))
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ data class DuplicateUserEmailException(
data class DuplicateUserNickNameException(
override val message: String = "이미 존재 하는 닉네임 입니다."
) : ServerException(400, message)

data class SendEmailException(
override val message: String = "이메일 전송에 실패 했습니다."
) : ServerException(500, message)

data class SkkuEmailException(
override val message: String = "성균관대 이메일이 아닙니다."
) : ServerException(400, message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.restaurant.be.user.domain.service

import com.restaurant.be.common.exception.SendEmailException
import com.restaurant.be.common.exception.SkkuEmailException
import com.restaurant.be.common.redis.RedisRepository
import com.restaurant.be.user.presentation.dto.SendEmailRequest
import com.restaurant.be.user.presentation.dto.common.EmailSendType
import com.restaurant.be.user.repository.EmailRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit

@Service
class ValidateEmailService(
private val emailRepository: EmailRepository,
@Value("\${aws.sender-email}") private val emailSource: String,
private val redisRepository: RedisRepository
) {
private val signUpTemplate =
EmailRepository::class.java.classLoader
.getResource("sign-up-template.html")!!
.readText()
private val resetPasswordTemplate =
EmailRepository::class.java.classLoader
.getResource("reset-password-template.html")!!
.readText()

fun sendValidateCode(request: SendEmailRequest) {
if (request.email.split("@")[1] != "g.skku.edu") {
throw SkkuEmailException()
}

try {
val code = emailRepository.generateRandomCode()
val email = request.email
if (request.sendType == EmailSendType.SIGN_UP) {
val message = software.amazon.awssdk.services.ses.model.SendEmailRequest
.builder()
.source(emailSource)
.destination {
it.toAddresses(email)
}
.message {
it.subject {
it.data("먹꾸스꾸 회원가입 인증번호입니다.")
}
it.body {
it.html {
it.data(signUpTemplate.replace("AUTHENTICATION_CODE", code))
}
}
}.build()

redisRepository.setValue("user:$email:signUpCode", code, 3, TimeUnit.MINUTES)
emailRepository.sendEmail(message)
} else {
val message = software.amazon.awssdk.services.ses.model.SendEmailRequest
.builder()
.source(emailSource)
.destination {
it.toAddresses(email)
}
.message {
it.subject {
it.data("먹꾸스꾸 비밀번호 재설정 코드입니다.")
}
it.body {
it.html {
it.data(resetPasswordTemplate.replace("AUTHENTICATION_CODE", code))
}
}
}.build()

redisRepository.setValue("user:$email:resetPasswordCode", code, 3, TimeUnit.MINUTES)
emailRepository.sendEmail(message)
}
} catch (e: Exception) {
throw SendEmailException()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.restaurant.be.user.presentation.controller

import com.restaurant.be.common.response.CommonResponse
import com.restaurant.be.user.domain.service.ValidateEmailService
import com.restaurant.be.user.presentation.dto.SendEmailRequest
import com.restaurant.be.user.presentation.dto.ValidateEmailRequest
import com.restaurant.be.user.presentation.dto.ValidateEmailResponse
Expand All @@ -18,7 +19,9 @@ import javax.validation.Valid
@Api(tags = ["01. User Info"], description = "유저 서비스")
@RestController
@RequestMapping("/v1/users/email")
class ValidateEmailController {
class ValidateEmailController(
private val validateEmailService: ValidateEmailService
) {
@PostMapping("/send")
@ApiOperation(value = "이메일 전송 API")
@ApiResponse(
Expand All @@ -29,6 +32,7 @@ class ValidateEmailController {
@Valid @RequestBody
request: SendEmailRequest
): CommonResponse<Unit> {
validateEmailService.sendValidateCode(request)
return CommonResponse.success()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ data class SendEmailRequest(

@ApiModelProperty(
value = "이메일 전송 타입",
example = "EMAIL_VALIDATION",
example = "SIGN_UP",
required = true,
allowableValues = "EMAIL_VALIDATION, UPDATE_PASSWORD"
)
val sendType: EmailSendType
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ data class ValidateEmailRequest(
val code: String,
@ApiModelProperty(
value = "이메일 전송 타입",
example = "EMAIL_VALIDATION",
required = true,
allowableValues = "EMAIL_VALIDATION, UPDATE_PASSWORD"
example = "SIGN_UP",
required = true
)
val sendType: EmailSendType
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.restaurant.be.user.presentation.dto.common

enum class EmailSendType {
EMAIL_VALIDATION,
UPDATE_PASSWORD
SIGN_UP,
RESET_PASSWORD
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.restaurant.be.user.repository

import kotlinx.coroutines.runBlocking
import org.springframework.stereotype.Repository
import software.amazon.awssdk.services.ses.SesClient
import software.amazon.awssdk.services.ses.model.SendEmailRequest
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.random.Random

@Repository
class EmailRepository(
private val sesClient: SesClient

) {
private val rnd = Random(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC))

fun sendEmail(sendEmailRequest: SendEmailRequest) {
runBlocking {
sesClient.sendEmail(sendEmailRequest)
}
}

fun generateRandomCode(): String {
return String.format("%06d", rnd.nextInt(0, 1000000))
}
}
7 changes: 6 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ jwt:
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
access-token-validity-in-seconds: 7200
refresh-token-validity-in-seconds: 1209600
refresh-token-validity-in-seconds: 1209600

aws:
accessKey: ${AWS_ACCESS_KEY:test}
secretKey: ${AWS_SECRET_KEY:test}
sender-email: ${SENDER_EMAIL:[email protected]}
22 changes: 22 additions & 0 deletions src/main/resources/reset-password-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="kr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Reset Template</title></head>
<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;">
<table style="background-color: #ffffff; width: 100%; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;">
<tr>
<td><p style="font-size: 32px; font-weight: bold; color: #000000; margin-bottom: 10px;">비밀번호 재설정 인증번호</p>
<p style="margin-bottom: 30px; color: #555555; line-height: 1.5;"> 안녕하세요. 먹꾸스꾸입니다. <br>
비밀번호 재설정을 위한 인증번호를 보내드립니다. </p>
<p style="margin-bottom: 30px; color: #555555; line-height: 1.5;"> 이메일 인증번호를 통해 인증을 완료해주세요. </p>
<p style="font-size: 40px; color: #db0d0d; font-weight: bold; padding: 20px 0;">AUTHENTICATION_CODE</p>
<p style="font-size: 14px; color: #aaaaaa; margin-top: 30px;"> 이 메일은 발신 전용 메일로 회신되지 않습니다.<br>
문의사항은 앱 내 문의하기를 이용해 주세요. </p>
<p style="font-size: 14px; color: #aaaaaa; margin-top: 30px;"> 혹시 직접 요청 하지 않은 인증 메일을 받으 셨을 경우 무시 해주시길 바랍니다. </p>
<p style="font-size: 14px; color: #aaaaaa; margin-top: 30px;"> 먹꾸스꾸 </p></td>
</tr>
</table>
</body>
</html>
22 changes: 22 additions & 0 deletions src/main/resources/sign-up-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="kr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SignUp Template</title></head>
<body style="font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;">
<table style="background-color: #ffffff; width: 100%; max-width: 600px; margin: 0 auto; padding: 20px; text-align: center;">
<tr>
<td><p style="font-size: 32px; font-weight: bold; color: #000000; margin-bottom: 10px;">회원가입 인증번호</p>
<p style="margin-bottom: 30px; color: #555555; line-height: 1.5;"> 안녕하세요. 먹꾸스꾸입니다. <br>
회원가입 위한 인증번호를 보내드립니다. </p>
<p style="margin-bottom: 30px; color: #555555; line-height: 1.5;"> 이메일 인증번호를 통해 인증을 완료해주세요. </p>
<p style="font-size: 40px; color: #db0d0d; font-weight: bold; padding: 20px 0;">AUTHENTICATION_CODE</p>
<p style="font-size: 14px; color: #aaaaaa; margin-top: 30px;"> 이 메일은 발신 전용 메일로 회신되지 않습니다.<br>
문의사항은 앱 내 문의하기를 이용해 주세요. </p>
<p style="font-size: 14px; color: #aaaaaa; margin-top: 30px;"> 혹시 직접 요청 하지 않은 인증 메일을 받으 셨을 경우 무시 해주시길 바랍니다. </p>
<p style="font-size: 14px; color: #aaaaaa; margin-top: 30px;"> 먹꾸스꾸 </p></td>
</tr>
</table>
</body>
</html>
7 changes: 6 additions & 1 deletion src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ jwt:
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
access-token-validity-in-seconds: 7200
refresh-token-validity-in-seconds: 1209600
refresh-token-validity-in-seconds: 1209600

aws:
accessKey: ${AWS_ACCESS_KEY:test}
secretKey: ${AWS_SECRET_KEY:test}
sender-email: ${SENDER_EMAIL:[email protected]}

0 comments on commit fd532e2

Please sign in to comment.