From 1766dfd7a267467e0651528fcef562fff0f6108b Mon Sep 17 00:00:00 2001 From: cabbage16 Date: Wed, 27 Nov 2024 22:35:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(#30):=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84=20-=20=ED=8A=B9?= =?UTF-8?q?=EC=A0=95=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A3=BC=EC=86=8C?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EB=8A=94=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EC=97=88=EC=96=B4=EC=9A=94.=20-=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=EB=A1=9C=20=EB=B0=9C=EC=86=A1=EB=90=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EB=B0=9B=EC=9C=BC=EB=A9=B4=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=9D=84=20=EB=A7=8C=EB=93=A4=EC=97=88=EC=96=B4=EC=9A=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../user/SendVerificationUseCase.java | 34 ++++++++++++++++ .../application/user/VerifyUseCase.java | 34 ++++++++++++++++ .../user/domain/SignUpVerification.java | 31 ++++++++++++++ .../VerificationCodeMismatchException.java | 10 +++++ .../VerifyingHasFailedException.java | 10 +++++ .../exception/error/UserErrorProperty.java | 6 ++- .../user/service/VerificationFacade.java | 21 ++++++++++ .../infrastructure/mail/MailService.java | 28 +++++++++++++ .../user/SignUpVerificationRepository.java | 7 ++++ .../user/VerificationRedisRepository.java | 6 +++ .../user/VerificationRedisRepositoryImpl.java | 23 +++++++++++ .../presentation/user/UserController.java | 40 +++++++++++++++++-- .../dto/request/SendVerificationRequest.java | 15 +++++++ .../user/dto/request/VerifyRequest.java | 22 ++++++++++ .../sinabro/shared/util/RandomCodeUtil.java | 10 +++++ src/main/resources/application.yml | 12 ++++++ 17 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/bamdoliro/sinabro/application/user/SendVerificationUseCase.java create mode 100644 src/main/java/com/bamdoliro/sinabro/application/user/VerifyUseCase.java create mode 100644 src/main/java/com/bamdoliro/sinabro/domain/user/domain/SignUpVerification.java create mode 100644 src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerificationCodeMismatchException.java create mode 100644 src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerifyingHasFailedException.java create mode 100644 src/main/java/com/bamdoliro/sinabro/domain/user/service/VerificationFacade.java create mode 100644 src/main/java/com/bamdoliro/sinabro/infrastructure/mail/MailService.java create mode 100644 src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/SignUpVerificationRepository.java create mode 100644 src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepository.java create mode 100644 src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepositoryImpl.java create mode 100644 src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/SendVerificationRequest.java create mode 100644 src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/VerifyRequest.java create mode 100644 src/main/java/com/bamdoliro/sinabro/shared/util/RandomCodeUtil.java diff --git a/build.gradle b/build.gradle index d31fdce..98f03f0 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,9 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'com.google.firebase:firebase-admin:9.2.0' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.apache.commons:commons-lang3:3.0' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/com/bamdoliro/sinabro/application/user/SendVerificationUseCase.java b/src/main/java/com/bamdoliro/sinabro/application/user/SendVerificationUseCase.java new file mode 100644 index 0000000..aa36241 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/application/user/SendVerificationUseCase.java @@ -0,0 +1,34 @@ +package com.bamdoliro.sinabro.application.user; + +import com.bamdoliro.sinabro.domain.user.domain.SignUpVerification; +import com.bamdoliro.sinabro.infrastructure.mail.MailService; +import com.bamdoliro.sinabro.infrastructure.persistence.user.SignUpVerificationRepository; +import com.bamdoliro.sinabro.presentation.user.dto.request.SendVerificationRequest; +import com.bamdoliro.sinabro.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@UseCase +public class SendVerificationUseCase { + + private final MailService mailService; + private final SignUpVerificationRepository signUpVerificationRepository; + + public void execute(SendVerificationRequest request) { + SignUpVerification signUpVerification = new SignUpVerification(request.getEmail()); + + String subject = "시나브로 회원가입 인증번호"; + String text = String.format( + "[시나브로] 회원가입 인증번호는 [%s]입니다.", + signUpVerification.getCode() + ); + + mailService.execute( + subject, + request.getEmail(), + text + ); + + signUpVerificationRepository.save(signUpVerification); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/application/user/VerifyUseCase.java b/src/main/java/com/bamdoliro/sinabro/application/user/VerifyUseCase.java new file mode 100644 index 0000000..f5906e7 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/application/user/VerifyUseCase.java @@ -0,0 +1,34 @@ +package com.bamdoliro.sinabro.application.user; + +import com.bamdoliro.sinabro.domain.user.domain.SignUpVerification; +import com.bamdoliro.sinabro.domain.user.exception.VerificationCodeMismatchException; +import com.bamdoliro.sinabro.domain.user.service.VerificationFacade; +import com.bamdoliro.sinabro.infrastructure.persistence.user.SignUpVerificationRepository; +import com.bamdoliro.sinabro.presentation.user.dto.request.VerifyRequest; +import com.bamdoliro.sinabro.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@UseCase +public class VerifyUseCase { + + private final SignUpVerificationRepository signUpVerificationRepository; + private final VerificationFacade verificationFacade; + + @Transactional + public void execute(VerifyRequest request) { + SignUpVerification signUpVerification = verificationFacade.getVerification(request.getEmail()); + System.out.println(request.getCode()); + System.out.println(signUpVerification.getCode()); + + if (!signUpVerification.getCode().equals(request.getCode())) { + throw new VerificationCodeMismatchException(); + } + + signUpVerificationRepository.updateSignUpVerification( + signUpVerification.getEmail(), + true + ); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/domain/user/domain/SignUpVerification.java b/src/main/java/com/bamdoliro/sinabro/domain/user/domain/SignUpVerification.java new file mode 100644 index 0000000..e5714eb --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/domain/user/domain/SignUpVerification.java @@ -0,0 +1,31 @@ +package com.bamdoliro.sinabro.domain.user.domain; + +import com.bamdoliro.sinabro.shared.util.RandomCodeUtil; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@RedisHash(value = "signup-verification", timeToLive = 60 * 5) +public class SignUpVerification { + + @Id + private String email; + + private String code; + + private Boolean isVerified; + + public SignUpVerification(String email) { + this.email = email; + this.code = RandomCodeUtil.generate(6); + this.isVerified = false; + } + + public void verify() { + this.isVerified = true; + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerificationCodeMismatchException.java b/src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerificationCodeMismatchException.java new file mode 100644 index 0000000..dd56f36 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerificationCodeMismatchException.java @@ -0,0 +1,10 @@ +package com.bamdoliro.sinabro.domain.user.exception; + +import com.bamdoliro.sinabro.domain.user.exception.error.UserErrorProperty; +import com.bamdoliro.sinabro.shared.error.SinabroException; + +public class VerificationCodeMismatchException extends SinabroException { + public VerificationCodeMismatchException() { + super(UserErrorProperty.VERIFICATION_CODE_MISMATCH); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerifyingHasFailedException.java b/src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerifyingHasFailedException.java new file mode 100644 index 0000000..ce28d4c --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/domain/user/exception/VerifyingHasFailedException.java @@ -0,0 +1,10 @@ +package com.bamdoliro.sinabro.domain.user.exception; + +import com.bamdoliro.sinabro.domain.user.exception.error.UserErrorProperty; +import com.bamdoliro.sinabro.shared.error.SinabroException; + +public class VerifyingHasFailedException extends SinabroException { + public VerifyingHasFailedException() { + super(UserErrorProperty.VERIFYING_HAS_FAILED); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/domain/user/exception/error/UserErrorProperty.java b/src/main/java/com/bamdoliro/sinabro/domain/user/exception/error/UserErrorProperty.java index d931b28..294ccdb 100644 --- a/src/main/java/com/bamdoliro/sinabro/domain/user/exception/error/UserErrorProperty.java +++ b/src/main/java/com/bamdoliro/sinabro/domain/user/exception/error/UserErrorProperty.java @@ -8,7 +8,11 @@ @Getter @AllArgsConstructor public enum UserErrorProperty implements ErrorProperty { - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자가 없습니다."); + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자가 없습니다."), + USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 가입한 사용자입니다."), + VERIFYING_HAS_FAILED(HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."), + VERIFICATION_CODE_MISMATCH(HttpStatus.UNAUTHORIZED, "인증코드가 일치하지 않습니다.") + ; private final HttpStatus status; private final String message; diff --git a/src/main/java/com/bamdoliro/sinabro/domain/user/service/VerificationFacade.java b/src/main/java/com/bamdoliro/sinabro/domain/user/service/VerificationFacade.java new file mode 100644 index 0000000..3ee2ef2 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/domain/user/service/VerificationFacade.java @@ -0,0 +1,21 @@ +package com.bamdoliro.sinabro.domain.user.service; + +import com.bamdoliro.sinabro.domain.user.domain.SignUpVerification; +import com.bamdoliro.sinabro.domain.user.exception.VerifyingHasFailedException; +import com.bamdoliro.sinabro.infrastructure.persistence.user.SignUpVerificationRepository; +import com.bamdoliro.sinabro.shared.annotation.UseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@UseCase +public class VerificationFacade { + + private final SignUpVerificationRepository signUpVerificationRepository; + + @Transactional(readOnly = true) + public SignUpVerification getVerification(String id) { + return signUpVerificationRepository.findById(id) + .orElseThrow(VerifyingHasFailedException::new); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/infrastructure/mail/MailService.java b/src/main/java/com/bamdoliro/sinabro/infrastructure/mail/MailService.java new file mode 100644 index 0000000..3c39276 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/infrastructure/mail/MailService.java @@ -0,0 +1,28 @@ +package com.bamdoliro.sinabro.infrastructure.mail; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class MailService { + + private final JavaMailSender javaMailSender; + + @Value("${spring.mail.username}") + private String from; + + public void execute(String subject, String to, String text) { + + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + + javaMailSender.send(message); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/SignUpVerificationRepository.java b/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/SignUpVerificationRepository.java new file mode 100644 index 0000000..7494d47 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/SignUpVerificationRepository.java @@ -0,0 +1,7 @@ +package com.bamdoliro.sinabro.infrastructure.persistence.user; + +import com.bamdoliro.sinabro.domain.user.domain.SignUpVerification; +import org.springframework.data.repository.CrudRepository; + +public interface SignUpVerificationRepository extends CrudRepository, VerificationRedisRepository { +} diff --git a/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepository.java b/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepository.java new file mode 100644 index 0000000..08e603b --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepository.java @@ -0,0 +1,6 @@ +package com.bamdoliro.sinabro.infrastructure.persistence.user; + +public interface VerificationRedisRepository { + + void updateSignUpVerification(String email, boolean verified); +} diff --git a/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepositoryImpl.java b/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepositoryImpl.java new file mode 100644 index 0000000..269e570 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/infrastructure/persistence/user/VerificationRedisRepositoryImpl.java @@ -0,0 +1,23 @@ +package com.bamdoliro.sinabro.infrastructure.persistence.user; + +import com.bamdoliro.sinabro.domain.user.domain.SignUpVerification; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.PartialUpdate; +import org.springframework.data.redis.core.RedisKeyValueTemplate; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class VerificationRedisRepositoryImpl implements VerificationRedisRepository { + + private final RedisKeyValueTemplate template; + + @Override + public void updateSignUpVerification(String email, boolean verified) { + PartialUpdate update = new PartialUpdate<>(email, SignUpVerification.class) + .set("isVerified", verified) + .refreshTtl(true); + + template.update(update); + } +} diff --git a/src/main/java/com/bamdoliro/sinabro/presentation/user/UserController.java b/src/main/java/com/bamdoliro/sinabro/presentation/user/UserController.java index 6dcd4cb..bbe3160 100644 --- a/src/main/java/com/bamdoliro/sinabro/presentation/user/UserController.java +++ b/src/main/java/com/bamdoliro/sinabro/presentation/user/UserController.java @@ -1,20 +1,54 @@ package com.bamdoliro.sinabro.presentation.user; +import com.bamdoliro.sinabro.application.user.SendVerificationUseCase; +import com.bamdoliro.sinabro.application.user.SignUpUseCase; +import com.bamdoliro.sinabro.application.user.VerifyUseCase; import com.bamdoliro.sinabro.domain.user.domain.User; +import com.bamdoliro.sinabro.presentation.user.dto.request.SendVerificationRequest; +import com.bamdoliro.sinabro.presentation.user.dto.request.SignUpRequest; +import com.bamdoliro.sinabro.presentation.user.dto.request.VerifyRequest; import com.bamdoliro.sinabro.presentation.user.dto.response.UserResponse; import com.bamdoliro.sinabro.shared.auth.AuthenticationPrincipal; import com.bamdoliro.sinabro.shared.response.CommonResponse; import com.bamdoliro.sinabro.shared.response.SingleCommonResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RequestMapping("/users") @RestController public class UserController { + private final SignUpUseCase signUpUseCase; + private final SendVerificationUseCase sendVerificationUseCase; + private final VerifyUseCase verifyUseCase; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public void signUp( + @RequestBody @Valid SignUpRequest request + ) { + signUpUseCase.execute(request); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/verify") + public void sendVerification( + @RequestBody @Valid SendVerificationRequest request + ) { + sendVerificationUseCase.execute(request); + } + + @ResponseStatus(HttpStatus.NO_CONTENT) + @PatchMapping("/verify") + public void verify( + @RequestBody @Valid VerifyRequest request + ) { + verifyUseCase.execute(request); + } + @GetMapping public SingleCommonResponse getUserInfo( @AuthenticationPrincipal User user diff --git a/src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/SendVerificationRequest.java b/src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/SendVerificationRequest.java new file mode 100644 index 0000000..e94112b --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/SendVerificationRequest.java @@ -0,0 +1,15 @@ +package com.bamdoliro.sinabro.presentation.user.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class SendVerificationRequest { + + @NotBlank(message = "필수값입니다.") + private String email; +} diff --git a/src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/VerifyRequest.java b/src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/VerifyRequest.java new file mode 100644 index 0000000..4491177 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/presentation/user/dto/request/VerifyRequest.java @@ -0,0 +1,22 @@ +package com.bamdoliro.sinabro.presentation.user.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class VerifyRequest { + + @NotBlank(message = "필수값입니다.") + @Email(message = "이메일 형식이어야 합니다") + private String email; + + @NotBlank(message = "필수값입니다.") + @Size(min = 6, max = 6, message = "6자여야 합니다.") + private String code; +} diff --git a/src/main/java/com/bamdoliro/sinabro/shared/util/RandomCodeUtil.java b/src/main/java/com/bamdoliro/sinabro/shared/util/RandomCodeUtil.java new file mode 100644 index 0000000..b707213 --- /dev/null +++ b/src/main/java/com/bamdoliro/sinabro/shared/util/RandomCodeUtil.java @@ -0,0 +1,10 @@ +package com.bamdoliro.sinabro.shared.util; + +import org.apache.commons.lang3.RandomStringUtils; + +public class RandomCodeUtil { + + public static String generate(int count) { + return RandomStringUtils.randomNumeric(count); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 006487b..8ec9ad4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -33,6 +33,18 @@ spring: connectTimeout: 60000 readTimeout: 300000 + mail: + host: ${MAIL_HOST} + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + auth: google: base-url: ${GOOGLE_BASE_URL}