-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 이메일 인증 기능 구현 #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
d78c0e1
fa96eaa
12b1af8
223b08b
ea2261f
60e4f13
b97e601
a86306f
997b46a
fd56900
4e642c1
e70aa50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,9 @@ dependencies { | |
| exclude group: 'io.swagger.core.v3', module: 'swagger-annotations' | ||
| } | ||
|
|
||
| implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' | ||
| implementation 'software.amazon.awssdk:ses:2.29.46' | ||
|
|
||
| compileOnly 'org.projectlombok:lombok' | ||
|
|
||
| developmentOnly 'org.springframework.boot:spring-boot-devtools' | ||
|
|
@@ -62,8 +65,9 @@ dependencies { | |
|
|
||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||
| testImplementation 'org.springframework.security:spring-security-test' | ||
| runtimeOnly 'org.postgresql:postgresql' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의존성 중복됩니다 제거해주세요 |
||
|
|
||
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||
| } | ||
|
|
||
| dependencyManagement { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.dreamteam.alter.adapter.inbound.general.email.dto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Schema(description = "이메일 인증 코드 발송 요청") | ||
| public class SendEmailVerificationCodeRequestDto { | ||
|
|
||
| @NotBlank | ||
| @Schema(description = "인증할 이메일 주소", example = "user@example.com") | ||
| private String email; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.dreamteam.alter.adapter.inbound.general.email.dto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Pattern; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Schema(description = "이메일 인증 코드 검증 요청") | ||
| public class VerifyEmailVerificationCodeRequestDto { | ||
|
|
||
| @NotBlank | ||
| @Schema(description = "인증할 이메일 주소", example = "user@example.com") | ||
| private String email; | ||
|
|
||
| @NotBlank | ||
| @Pattern(regexp = "^[0-9]{6}$", message = "인증 코드는 6자리 숫자여야 합니다.") | ||
| @Schema(description = "수신한 인증 코드 6자리", example = "123456") | ||
| private String code; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.dreamteam.alter.adapter.inbound.general.email.dto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Schema(description = "이메일 인증 성공 응답") | ||
| public class VerifyEmailVerificationCodeResponseDto { | ||
|
|
||
| @Schema(description = "이메일 인증 세션 토큰", example = "a1b2c3d4-e5f6-4a09-8c13-1b2a3d4e5f6a") | ||
| private String verificationToken; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,4 +51,8 @@ public class CreateUserRequestDto { | |
| @Schema(description = "생년월일", example = "YYYYMMDD") | ||
| private String birthday; | ||
|
|
||
| @NotBlank | ||
| @Schema(description = "이메일 인증 성공 후 받은 세션 토큰") | ||
| private String emailVerificationToken; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 가입 진행중인 회원의 정보 식별 시 |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.dreamteam.alter.adapter.outbound.aws.ses; | ||
|
|
||
| import com.dreamteam.alter.application.email.properties.EmailAuthProperties; | ||
| import com.dreamteam.alter.common.exception.CustomException; | ||
| import com.dreamteam.alter.common.exception.ErrorCode; | ||
| import com.dreamteam.alter.domain.email.port.outbound.EmailSenderPort; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import software.amazon.awssdk.services.ses.SesClient; | ||
| import software.amazon.awssdk.services.ses.model.*; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class SesEmailSenderAdapter implements EmailSenderPort { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AWS SES도 FCM과 동일한 외부 API 클라이언트이므로 기존 프로젝트 관례에 맞게 application 레이어로 이동해 주세요. 기존 패턴 (FCM) 변경 후 (SES) 클래스 이름도 |
||
|
|
||
| private final SesClient sesClient; | ||
| private final EmailAuthProperties emailProperties; | ||
|
|
||
| @Override | ||
| public void sendVerificationCode(String toEmail, String code) { | ||
| try { | ||
| String subject = "[ALTER] 이메일 인증 코드"; | ||
| String bodyText = "인증 코드: " + code + "\n\n이 코드는 5분간 유효합니다."; | ||
|
|
||
| SendEmailRequest request = SendEmailRequest.builder() | ||
| .source(emailProperties.getFrom()) | ||
| .destination(Destination.builder().toAddresses(toEmail).build()) | ||
| .message(Message.builder() | ||
| .subject(Content.builder().data(subject).build()) | ||
| .body(Body.builder() | ||
| .text(Content.builder().data(bodyText).build()) | ||
| .build()) | ||
| .build()) | ||
| .build(); | ||
|
|
||
| sesClient.sendEmail(request); | ||
| log.info("Sent verification email to: {}", toEmail); | ||
|
|
||
| } catch (SesException e) { | ||
| log.error("Failed to send SES email to {}: {}", toEmail, e.awsErrorDetails().errorMessage()); | ||
| throw new CustomException(ErrorCode.EMAIL_VERIFICATION_SEND_FAILED); | ||
| } catch (Exception e) { | ||
| log.error("Unexpected error sending email to {}: {}",toEmail, e.getMessage()); | ||
| throw new CustomException(ErrorCode.EMAIL_VERIFICATION_SEND_FAILED); | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.dreamteam.alter.adapter.outbound.aws.ses.config; | ||
|
|
||
| import com.dreamteam.alter.adapter.outbound.aws.ses.properties.AwsProperties; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.util.StringUtils; | ||
| 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; | ||
| import software.amazon.awssdk.services.ses.SesClientBuilder; | ||
|
|
||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| public class AwsSesConfig { | ||
|
|
||
| private final AwsProperties awsProperties; | ||
|
|
||
| @Bean | ||
| public SesClient sesClient() { | ||
| SesClientBuilder builder = SesClient.builder() | ||
| .region(Region.of(awsProperties.getRegion())); | ||
|
|
||
| if (StringUtils.hasText(awsProperties.getAccessKey()) && StringUtils.hasText(awsProperties.getSecretKey())) { | ||
| AwsBasicCredentials credentials = AwsBasicCredentials.create( | ||
| awsProperties.getAccessKey(), | ||
| awsProperties.getSecretKey() | ||
| ); | ||
| builder.credentialsProvider(StaticCredentialsProvider.create(credentials)); | ||
| } | ||
|
|
||
| return builder.build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.dreamteam.alter.adapter.outbound.aws.ses.properties; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @ConfigurationProperties(prefix = "aws") | ||
| public class AwsProperties { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희는 속성 값 주입을 위해 |
||
| private String region; | ||
| private String accessKey; | ||
| private String secretKey; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.dreamteam.alter.adapter.outbound.email.persistence; | ||
|
|
||
| import com.dreamteam.alter.domain.email.entity.EmailSendLog; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface EmailSendLogJpaRepository extends JpaRepository<EmailSendLog, Long> { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.dreamteam.alter.adapter.outbound.email.persistence; | ||
|
|
||
| import com.dreamteam.alter.domain.email.entity.EmailSendLog; | ||
| import com.dreamteam.alter.domain.email.port.outbound.EmailSendLogPort; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class EmailSendLogRepositoryImpl implements EmailSendLogPort { | ||
|
|
||
| private final EmailSendLogJpaRepository jpaRepository; | ||
|
|
||
| @Override | ||
| public EmailSendLog save(EmailSendLog log) { | ||
| return jpaRepository.save(log); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<EmailSendLog> findById(Long id) { | ||
| return jpaRepository.findById(id); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용처 없는 것 같은데 확인하고 제거해주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이메일 템플릿 사용시에 사용 예정인 의존성입니다