Skip to content

Commit 3444139

Browse files
authored
Merge pull request #51 from LearnMate-Dev/feat/#49
[Feat/#49] 자체 로그인 이메일 인증 기능 구현
2 parents fd59d63 + 1beafd5 commit 3444139

File tree

13 files changed

+281
-4
lines changed

13 files changed

+281
-4
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ dependencies {
5656
// coroutine
5757
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
5858
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
59+
// email
60+
implementation ("org.springframework.boot:spring-boot-starter-mail")
61+
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
5962

6063
compileOnly("org.projectlombok:lombok")
6164
annotationProcessor("org.projectlombok:lombok")

src/main/kotlin/learn_mate_it/dev/common/base/BaseEntity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ abstract class BaseEntity {
1919

2020
@LastModifiedDate
2121
@Column(nullable = false)
22-
private var updatedAt: LocalDateTime? = null
22+
protected var updatedAt: LocalDateTime? = null
2323

2424
fun getCreatedAtFormatted(): String {
2525
val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd")

src/main/kotlin/learn_mate_it/dev/common/status/ErrorStatus.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,22 @@ enum class ErrorStatus (
2727
APPLE_LOGIN_PUB_KEY_CLIENT_ERROR(HttpStatus.BAD_REQUEST, "400", "애플 로그인을 위한 공개키 조회 중 클라이언트 오류가 발생했습니다."),
2828
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "400", "비밀번호가 일치하지 않습니다."),
2929
INVALID_PASSWORD_FORMAT(HttpStatus.BAD_REQUEST, "400", "비밀번호 형식이 올바르지 않습니다. 8자 이상 영소문자, 숫자, 특수문자를 1개 이상 포함해주세요."),
30+
INVALID_EMAIL_VERIFICATION_CODE(HttpStatus.BAD_REQUEST, "400", "잘못된 이메일 인증 번호입니다."),
31+
EMAIL_IS_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "400", "인증이 완료되지 않은 이메일 주소입니다."),
3032
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "401", "유효하지 않은 액세스 토큰입니다."),
3133
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "401", "유효하지 않은 리프레시 토큰입니다."),
3234
EXPIRED_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "401", "토큰이 만료되었습니다."),
3335
APPLE_LOGIN_NO_MATCHING_PUB_KEY(HttpStatus.NOT_FOUND, "404", "애플 로그인을 위한 일치하는 공개키가 존재하지 않습니다."),
3436
NOT_FOUND_REFRESH_TOKEN(HttpStatus.NOT_FOUND, "404", "존재하지 않는 리프레시 토큰입니다."),
3537
NOT_FOUND_USER(HttpStatus.NOT_FOUND, "404", "존재하지 않는 유저입니다."),
38+
NOT_FOUND_EMAIL_TO_VERIFICATION(HttpStatus.NOT_FOUND, "404", "해당 계정의 이메일 인증 요청 정보가 존재하지 않습니다."),
3639
ALREADY_ACCOUNT_EXIST(HttpStatus.CONFLICT, "409", "이미 해당 이메일로 가입한 계정이 존재합니다."),
3740
SOCIAL_LOGIN_USER(HttpStatus.CONFLICT, "409", "해당 이메일은 소셜 로그인으로 가입된 계정이 존재합니다. 소셜 로그인을 이용해주세요."),
41+
EXPIRED_EMAIL_VERIFICATION_CODE(HttpStatus.CONFLICT, "409", "이메일 인증 번호가 만료되었습니다. 다시 시도해주세요."),
3842
APPLE_LOGIN_PUB_KEY_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "애플 로그인을 위한 공개키 조회 중 서버 오류가 발생했습니다."),
3943
APPLE_LOGIN_KID_DECODE_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "애플 로그인을 위한 Key ID 추출 과정에서 서버 오류가 발생했습니다."),
4044
APPLE_LOGIN_VERIFY_IDENTITY_TOKEN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "애플 로그인을 위한 Identity Token 검증 과정에서 오류가 발생했습니다."),
45+
SEND_VERIFICATION_CODE_EMAIL_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "이메일 인증 코드 발송 중 오류가 발생했습니다."),
4146

4247
/**
4348
* Course

src/main/kotlin/learn_mate_it/dev/common/status/SuccessStatus.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ enum class SuccessStatus (
2020
USER_LOGOUT_SUCCESS(HttpStatus.OK, "200", "로그아웃이 성공적으로 완료되었습니다."),
2121
DELETE_USER_SUCCESS(HttpStatus.OK, "200", "회원탈퇴가 성공적으로 완료되었습니다."),
2222
GET_USER_PROFILE_SUCCESS(HttpStatus.OK, "200", "유저 프로필 조회가 성공적으로 완료되었습니다."),
23+
SEND_EMAIL_VERIFICATION_CODE_SUCCESS(HttpStatus.OK, "200", "이메일 인증 코드 발송이 성공적으로 완료되었습니다."),
24+
VERIFY_EMAIL_SUCCESS(HttpStatus.OK, "200", "이메일 인증이 성공적으로 완료되었습니다."),
2325

2426
/**
2527
* Course

src/main/kotlin/learn_mate_it/dev/domain/auth/application/dto/request/AuthRequest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,21 @@ data class SignInRequest(
3333
@field:NotBlank(message = "비밀번호는 필수입니다.")
3434
@field:Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
3535
val password: String
36+
)
37+
38+
data class EmailVerificationRequest(
39+
@field:NotBlank(message = "이메일 주소는 필수입니다.")
40+
@field:Email(message = "이메일 형식이 올바르지 않습니다.")
41+
@field:Size(max = 30, message = "이메일은 30자를 초과할 수 없습니다.")
42+
val email: String
43+
)
44+
45+
data class ConfirmEmailVerificationRequest(
46+
@field:NotBlank(message = "이메일 주소는 필수입니다.")
47+
@field:Email(message = "이메일 형식이 올바르지 않습니다.")
48+
@field:Size(max = 30, message = "이메일은 30자를 초과할 수 없습니다.")
49+
val email: String,
50+
51+
@field:NotBlank(message = "인증 코드는 필수입니다.")
52+
val code: String
3653
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package learn_mate_it.dev.domain.auth.application.service
2+
3+
interface EmailVerificationService {
4+
5+
fun sendVerificationCodeToEmail(email: String)
6+
fun confirmEmailVerificationCode(email: String, code: String)
7+
fun validateIsEmailVerified(email: String)
8+
9+
}

src/main/kotlin/learn_mate_it/dev/domain/auth/application/service/impl/AuthServiceImpl.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package learn_mate_it.dev.domain.auth.application.service.impl
22

33
import io.jsonwebtoken.Claims
4+
import jakarta.transaction.Transactional
45
import learn_mate_it.dev.common.exception.GeneralException
56
import learn_mate_it.dev.common.status.ErrorStatus
67
import learn_mate_it.dev.domain.auth.application.dto.request.AppleLoginRequest
@@ -9,6 +10,7 @@ import learn_mate_it.dev.domain.auth.application.dto.request.SignUpRequest
910
import learn_mate_it.dev.domain.auth.application.dto.response.TokenResponse
1011
import learn_mate_it.dev.domain.auth.application.service.AppleClient
1112
import learn_mate_it.dev.domain.auth.application.service.AuthService
13+
import learn_mate_it.dev.domain.auth.application.service.EmailVerificationService
1214
import learn_mate_it.dev.domain.auth.application.service.TokenService
1315
import learn_mate_it.dev.domain.auth.infra.application.dto.response.Key
1416
import learn_mate_it.dev.domain.auth.jwt.JwtUtil
@@ -33,15 +35,18 @@ class AuthServiceImpl(
3335
private val userRepository: UserRepository,
3436
private val passwordEncoder: PasswordEncoder,
3537
private val tokenService: TokenService,
38+
private val emailVerificationService: EmailVerificationService,
3639
private val jwtUtil: JwtUtil
3740
): AuthService {
3841

3942
/**
4043
* Sign-Up with Email, Username, Pwd
4144
*/
45+
@Transactional
4246
override fun signUp(request: SignUpRequest) {
4347
checkEmailExist(request.email)
4448
checkPwdPatternIsValid(request.password)
49+
emailVerificationService.validateIsEmailVerified(request.email)
4550

4651
userRepository.save(
4752
User(
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package learn_mate_it.dev.domain.auth.application.service.impl
2+
3+
import jakarta.transaction.Transactional
4+
import learn_mate_it.dev.common.exception.GeneralException
5+
import learn_mate_it.dev.common.status.ErrorStatus
6+
import learn_mate_it.dev.domain.auth.application.service.EmailVerificationService
7+
import learn_mate_it.dev.domain.auth.domain.model.EmailVerification
8+
import learn_mate_it.dev.domain.auth.domain.repository.EmailVerificationRepository
9+
import learn_mate_it.dev.domain.auth.infra.application.service.EmailSendService
10+
import org.springframework.stereotype.Service
11+
12+
@Service
13+
class EmailVerificationServiceImpl(
14+
private val emailSendService: EmailSendService,
15+
private val emailVerificationRepository: EmailVerificationRepository
16+
): EmailVerificationService {
17+
18+
/**
19+
* Send Verification Code To Email
20+
*/
21+
@Transactional
22+
override fun sendVerificationCodeToEmail(email: String) {
23+
val code = createVerificationCode()
24+
val verification = emailVerificationRepository.findByEmail(email)
25+
?: EmailVerification(email, code)
26+
27+
verification.updateCode(code)
28+
emailVerificationRepository.save(verification)
29+
30+
emailSendService.sendEmail(email, code)
31+
}
32+
33+
private fun createVerificationCode() = (100000..999999).random().toString()
34+
35+
36+
/**
37+
* Verify Email Verification Code
38+
*/
39+
@Transactional
40+
override fun confirmEmailVerificationCode(email: String, code: String) {
41+
val verification = getEmailVerificationByEmail(email)
42+
43+
if (verification.isExpired()) {
44+
throw GeneralException(ErrorStatus.EXPIRED_EMAIL_VERIFICATION_CODE)
45+
}
46+
47+
if (verification.code != code) {
48+
throw GeneralException(ErrorStatus.INVALID_EMAIL_VERIFICATION_CODE)
49+
}
50+
51+
verification.verify()
52+
}
53+
54+
@Transactional
55+
override fun validateIsEmailVerified(email: String) {
56+
val verification = getEmailVerificationByEmail(email)
57+
verification.ensureIsNotVerified()
58+
59+
emailVerificationRepository.delete(verification)
60+
}
61+
62+
private fun getEmailVerificationByEmail(email: String): EmailVerification {
63+
return emailVerificationRepository.findByEmail(email)
64+
?: throw GeneralException(ErrorStatus.NOT_FOUND_EMAIL_TO_VERIFICATION)
65+
}
66+
67+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package learn_mate_it.dev.domain.auth.domain.model
2+
3+
import jakarta.persistence.*
4+
import learn_mate_it.dev.common.base.BaseEntity
5+
import learn_mate_it.dev.common.exception.GeneralException
6+
import learn_mate_it.dev.common.status.ErrorStatus
7+
import java.time.LocalDateTime
8+
9+
@Entity
10+
data class EmailVerification(
11+
12+
@Column(nullable = false)
13+
val email: String,
14+
15+
@Column(nullable = false)
16+
var code: String
17+
18+
): BaseEntity() {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
val emailVerificationId: Long = 0L
23+
24+
@Column(nullable = false)
25+
var isVerified: Boolean = false
26+
27+
fun verify() {
28+
this.isVerified = true
29+
}
30+
31+
fun updateCode(code: String) {
32+
this.code = code
33+
}
34+
35+
fun isExpired(): Boolean {
36+
val expirationTime = this.updatedAt!!.plusMinutes(5L)
37+
return LocalDateTime.now().isAfter(expirationTime)
38+
}
39+
40+
fun ensureIsNotVerified() {
41+
if (!this.isVerified) {
42+
throw GeneralException(ErrorStatus.EMAIL_IS_NOT_VERIFIED)
43+
}
44+
}
45+
46+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package learn_mate_it.dev.domain.auth.domain.repository
2+
3+
import learn_mate_it.dev.domain.auth.domain.model.EmailVerification
4+
import org.springframework.data.jpa.repository.JpaRepository
5+
6+
interface EmailVerificationRepository: JpaRepository<EmailVerification, Long> {
7+
fun findByEmail(email: String): EmailVerification?
8+
}

0 commit comments

Comments
 (0)