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}