Skip to content

Commit

Permalink
feat(#30): 이메일 인증 구현
Browse files Browse the repository at this point in the history
- 특정 이메일 주소로 인증코드를 보내는 기능을 만들었어요.
- 이메일로 발송된 인증코드를 입력받으면 인증 완료하는 기능을 만들었어요.
  • Loading branch information
cabbage16 committed Nov 27, 2024
1 parent 9b3d4f9 commit 1766dfd
Show file tree
Hide file tree
Showing 17 changed files with 308 additions and 4 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<SignUpVerification, String>, VerificationRedisRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bamdoliro.sinabro.infrastructure.persistence.user;

public interface VerificationRedisRepository {

void updateSignUpVerification(String email, boolean verified);
}
Original file line number Diff line number Diff line change
@@ -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<SignUpVerification> update = new PartialUpdate<>(email, SignUpVerification.class)
.set("isVerified", verified)
.refreshTtl(true);

template.update(update);
}
}
Original file line number Diff line number Diff line change
@@ -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<UserResponse> getUserInfo(
@AuthenticationPrincipal User user
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 12 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down

0 comments on commit 1766dfd

Please sign in to comment.