-
Notifications
You must be signed in to change notification settings - Fork 0
이메일 인증 기능 구현 #9
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
이메일 인증 기능 구현 #9
Changes from 16 commits
b7d8c03
b0d5e75
51ce29f
79942e1
f02ecdb
1a5653d
9065c9c
39cf97c
77ee375
6c672d0
7c54617
778d8c6
2037fec
79f27a5
a856bfc
b2f0b14
95bd04f
9dab2e1
7787679
028e202
7b9c962
c520a2c
334075e
61dc311
5200658
e49d0e0
8fb525f
2b42387
9699ed3
78070cf
88ba256
6ab30b2
cc63744
4057cd6
f32e1c9
f3af5d7
b8088ba
0bcc57c
80b7d29
afe7b1a
8947267
b8decdf
6287c15
3028cb3
9571324
bbbd82d
13f9762
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 |
|---|---|---|
|
|
@@ -3,8 +3,6 @@ name: Test and Build | |
| on: | ||
| pull_request: | ||
| branches: ["*"] | ||
| push: | ||
| branches: ["develop"] | ||
|
|
||
| jobs: | ||
| gradle: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.ampersand.groom.domain.auth.application.port; | ||
|
|
||
| import com.ampersand.groom.domain.auth.domain.model.EmailVerification; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.Optional; | ||
|
|
||
| public interface EmailVerificationPort { | ||
|
|
||
| // 인증 정보 저장 | ||
| EmailVerification save(EmailVerification emailVerification); | ||
|
|
||
| // 인증 코드로 이메일 조회 | ||
| Optional<EmailVerification> findByCode(String code); | ||
|
|
||
| // 이메일로 인증 정보 조회 | ||
| Optional<EmailVerification> findByEmail(String email); | ||
|
|
||
| // 만료된 인증 정보 삭제 | ||
| void deleteAllExpired(LocalDateTime now); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package com.ampersand.groom.domain.auth.application.service; | ||
|
|
||
| import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort; | ||
| import com.ampersand.groom.domain.auth.domain.model.EmailVerification; | ||
| import com.ampersand.groom.domain.auth.expection.AuthException; | ||
| import org.springframework.mail.SimpleMailMessage; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.scheduling.annotation.Scheduled; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.Random; | ||
|
|
||
| @Service | ||
| public class EmailVerificationService { | ||
|
|
||
| private final EmailVerificationPort emailVerificationPort; | ||
| private final JavaMailSender javaMailSender; | ||
|
|
||
| public EmailVerificationService(EmailVerificationPort emailVerificationPort, JavaMailSender javaMailSender) { | ||
| this.emailVerificationPort = emailVerificationPort; | ||
| this.javaMailSender = javaMailSender; | ||
| } | ||
|
|
||
| //6자리 숫자 인증 코드 생성 | ||
| private String generateVerificationCode() { | ||
| Random random = new Random(); | ||
| int code = 10000000 + random.nextInt(90000000); | ||
| return String.valueOf(code); | ||
| } | ||
|
||
|
|
||
| // 이메일 전송 메서드 | ||
| private void sendEmail(String to, String subject, String text) { | ||
| SimpleMailMessage message = new SimpleMailMessage(); | ||
| message.setTo(to); | ||
| message.setSubject(subject); | ||
| message.setText(text); | ||
| javaMailSender.send(message); | ||
| } | ||
|
|
||
| // 회원가입 인증 이메일 전송 | ||
| public void sendSignupVerificationEmail(String email) { | ||
| String code = generateVerificationCode(); | ||
| sendEmail(email, "회원가입 인증", "귀하의 인증 코드는: " + code); | ||
|
|
||
| EmailVerification emailVerification = new EmailVerification(email, code); | ||
| emailVerificationPort.save(emailVerification); | ||
| } | ||
|
|
||
| // 비밀번호 변경을 위한 인증 이메일 전송 | ||
| public void sendPasswordResetEmail(String email) { | ||
| String code = generateVerificationCode(); | ||
| sendEmail(email, "비밀번호 변경 인증", "귀하의 인증 코드는: " + code); | ||
|
|
||
| EmailVerification emailVerification = new EmailVerification(email, code); | ||
| emailVerificationPort.save(emailVerification); | ||
| } | ||
|
|
||
| // 인증 코드 검증 | ||
| public boolean verifyEmailCode(String code) { | ||
| EmailVerification emailVerification = emailVerificationPort.findByCode(code) | ||
| .orElseThrow(() -> new AuthException("Invalid or expired verification code")); | ||
|
||
|
|
||
| emailVerification.setVerified(true); | ||
| emailVerificationPort.save(emailVerification); | ||
| return true; | ||
| } | ||
|
|
||
| // 만료된 인증 정보 삭제(1시간) | ||
| @Scheduled(fixedRate = 3600000) | ||
| public void deleteExpiredVerifications() { | ||
| emailVerificationPort.deleteAllExpired(LocalDateTime.now()); | ||
| } | ||
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.ampersand.groom.domain.auth.application.usecase; | ||
|
|
||
| import com.ampersand.groom.domain.auth.application.service.EmailVerificationService; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
|
||
| public class EmailVerificationUseCase { | ||
|
|
||
| private final EmailVerificationService emailVerificationService; | ||
|
|
||
| public EmailVerificationUseCase(EmailVerificationService emailVerificationService) { | ||
| this.emailVerificationService = emailVerificationService; | ||
| } | ||
|
|
||
| // 회원가입 인증 이메일 전송 | ||
| public void executeSendSignupVerificationEmail(String email) { | ||
| emailVerificationService.sendSignupVerificationEmail(email); | ||
| } | ||
|
|
||
| // 비밀번호 변경 인증 이메일 전송 | ||
| public void executeSendPasswordResetEmail(String email) { | ||
| emailVerificationService.sendPasswordResetEmail(email); | ||
| } | ||
|
|
||
| // 이메일 인증 코드 검증 | ||
| public boolean executeVerifyEmail(String code) { | ||
| return emailVerificationService.verifyEmailCode(code); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.ampersand.groom.domain.auth.domain.model; | ||
|
||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Setter | ||
|
||
| @NoArgsConstructor | ||
| @Table(name = "email") | ||
| @ToString | ||
| public class EmailVerification { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
| @Column(nullable = false) | ||
| private String email; | ||
| @Column(nullable = false) | ||
| private String code; | ||
| @Column(nullable = false) | ||
| private boolean isVerified; | ||
| @Column(nullable = false) | ||
| private LocalDateTime verificationDate; | ||
|
|
||
| public EmailVerification(String email, String code) { | ||
| this.email = email; | ||
| this.code = code; | ||
| this.isVerified = false; | ||
| this.verificationDate = LocalDateTime.now().plusMinutes(5); | ||
| } | ||
|
|
||
| @Builder | ||
| public EmailVerification(Long id, String email, String code, boolean isVerified, LocalDateTime verificationDate) { | ||
| this.id = id; | ||
| this.email = email; | ||
| this.code = code; | ||
| this.isVerified = isVerified; | ||
| this.verificationDate = verificationDate; | ||
| } | ||
|
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.ampersand.groom.domain.auth.expection; | ||
|
|
||
| public class AuthException extends RuntimeException { | ||
| public AuthException(String message) { | ||
| super(message); | ||
| } | ||
| } | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.ampersand.groom.domain.auth.persistence.adapter; | ||
|
||
|
|
||
| import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort; | ||
| import com.ampersand.groom.domain.auth.domain.model.EmailVerification; | ||
| import com.ampersand.groom.domain.auth.persistence.adapter.email.SpringDataEmailVerificationRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.Optional; | ||
|
|
||
| @Repository | ||
| public class JpaEmailVerificationRepository implements EmailVerificationPort { | ||
|
||
|
|
||
| private final SpringDataEmailVerificationRepository repository; | ||
|
|
||
| public JpaEmailVerificationRepository(SpringDataEmailVerificationRepository repository) { | ||
| this.repository = repository; | ||
| } | ||
|
|
||
| @Override | ||
| public EmailVerification save(EmailVerification emailVerification) { | ||
| return repository.save(emailVerification); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<EmailVerification> findByCode(String code) { | ||
| return repository.findByCode(code); | ||
| } | ||
|
|
||
| @Override | ||
| public Optional<EmailVerification> findByEmail(String email) { | ||
| return repository.findByEmail(email); | ||
| } | ||
|
|
||
| @Override | ||
| public void deleteAllExpired(LocalDateTime now) { | ||
| repository.deleteAllExpired(now); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package com.ampersand.groom.domain.auth.persistence.adapter.email; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.mail.javamail.JavaMailSenderImpl; | ||
|
|
||
| import java.util.Properties; | ||
|
|
||
| @Configuration | ||
| public class EmailConfig { | ||
|
|
||
| @Value("${email.host}") | ||
| private String host; | ||
|
|
||
| @Value("${email.port}") | ||
| private int port; | ||
|
|
||
| @Value("${email.username}") | ||
| private String username; | ||
|
|
||
| @Value("${email.password}") | ||
| private String password; | ||
|
|
||
| @Bean | ||
| public JavaMailSender emailSender() { | ||
| JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); | ||
| mailSender.setHost(host); | ||
| mailSender.setPort(port); | ||
| mailSender.setUsername(username); | ||
| mailSender.setPassword(password); | ||
|
|
||
| Properties properties = new Properties(); | ||
| properties.put("mail.smtp.auth", "true"); | ||
| properties.put("mail.smtp.starttls.enable", "true"); | ||
| mailSender.setJavaMailProperties(properties); | ||
|
|
||
| return mailSender; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.ampersand.groom.domain.auth.persistence.adapter.email; | ||
|
|
||
| import com.ampersand.groom.domain.auth.domain.model.EmailVerification; | ||
| import io.lettuce.core.dynamic.annotation.Param; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Modifying; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.stereotype.Repository; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.Optional; | ||
|
|
||
| @Repository | ||
| public interface SpringDataEmailVerificationRepository extends JpaRepository<EmailVerification, Long> { | ||
|
|
||
| Optional<EmailVerification> findByCode(String code); | ||
|
|
||
| Optional<EmailVerification> findByEmail(String email); | ||
|
|
||
| @Transactional | ||
| @Modifying | ||
| @Query("DELETE FROM EmailVerification e WHERE e.verificationDate < :now") | ||
| void deleteAllExpired(@Param("now") LocalDateTime now); | ||
| } | ||
|
Member
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. 애플리케이션의 처리량도 그렇게 많지 않고 복잡한 쿼리도 아니여서 JPQL을 이용해서 사용해도 될것 같은데...프로젝트 통일성을 위해서 QueryDSL을 사용해보는건 어떤가요?...?
Contributor
Author
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,62 @@ | ||
| package com.ampersand.groom.domain.auth.presentation.controller; | ||
|
|
||
| import com.ampersand.groom.domain.auth.application.usecase.EmailVerificationUseCase; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/auth") | ||
| public class AuthController { | ||
|
|
||
| private final EmailVerificationUseCase emailVerificationUseCase; | ||
|
|
||
| public AuthController(EmailVerificationUseCase emailVerificationUseCase) { | ||
| this.emailVerificationUseCase = emailVerificationUseCase; | ||
| } | ||
|
||
|
|
||
| @PostMapping("/email/verify-email") | ||
|
||
| public ResponseEntity<?> verifyEmail(@RequestBody Map<String, Object> body) { | ||
| String code = (String) body.get("code"); | ||
|
|
||
| if (code == null || code.length() != 8) { | ||
|
||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid verification code format."); | ||
| } | ||
|
|
||
| boolean result = emailVerificationUseCase.executeVerifyEmail(code); | ||
| if (result) { | ||
| return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification successful."); | ||
| } else { | ||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired verification code"); | ||
| } | ||
| } | ||
|
|
||
| @PostMapping("/email/signup") | ||
| public ResponseEntity<?> signup(@RequestBody Map<String, Object> body) { | ||
| String email = (String) body.get("email"); | ||
|
|
||
| if (email == null || email.length() > 16) { | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid email format."); | ||
| } | ||
|
|
||
| emailVerificationUseCase.executeSendSignupVerificationEmail(email); | ||
| return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification email sent"); | ||
| } | ||
|
|
||
| @PostMapping("/email/refresh") | ||
| public ResponseEntity<?> refresh(@RequestBody Map<String, Object> body) { | ||
| String email = (String) body.get("email"); | ||
|
|
||
| if (email == null || email.length() > 16) { | ||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid email format."); | ||
| } | ||
|
|
||
| emailVerificationUseCase.executeSendPasswordResetEmail(email); | ||
| return ResponseEntity.status(HttpStatus.RESET_CONTENT).body("Verification email sent"); | ||
| } | ||
| } | ||
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.
현재 생성자 주입을 사용하고 있는데, Lombok의
@RequiredArgsConstructor를 사용하시면 가독성과 유지보수 측면에서 더 좋을 것 같아요.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.
9dab2e1
피드백 반영 하였습니다.
리뷰 감사합니다!