Skip to content
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

이메일 인증 API 생성 #324

Merged
merged 6 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@ControllerAdvice
public class GlobalExceptionHandler {
Expand Down Expand Up @@ -78,6 +79,18 @@ ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}

@ExceptionHandler(NoResourceFoundException.class)
ResponseEntity<ErrorResponse> handleNoResourceFoundException(NoResourceFoundException exception) {
ErrorResponse errorResponse =
ErrorResponse.builder()
.status(HttpStatus.NOT_FOUND.getReasonPhrase())
.statusCode(HttpStatus.NOT_FOUND.value())
.message(exception.getMessage())
.build();

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}

private void printLog(String logMessage, LogLevel logLevel) {
switch (logLevel) {
case ERROR -> logger.error(logMessage);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,61 @@
package io.perfume.api.common.config;

import java.util.Properties;
import mailer.MailSender;
import mailer.impl.SESMailSender;
import mailer.impl.MailSenderImpl;
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;

@Configuration
public class MailSenderConfiguration {
@Value("${spring.mail.host}")
private String host;

@Value("${spring.mail.port}")
private int port;

@Value("${spring.mail.username}")
private String username;

@Value("${spring.mail.password}")
private String password;

@Value("${spring.mail.properties.mail.smtp.auth}")
private String auth;

@Value("${spring.mail.properties.mail.smtp.timeout}")
private String timeout;

@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private String starttls;

@Bean
public MailSender mailSender() {
return new SESMailSender();
String fromEmail = username + host.split("\\.")[1];
return new MailSenderImpl(javaMailSender(), fromEmail);
}

@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();

javaMailSender.setHost(host);
javaMailSender.setPort(port);
javaMailSender.setUsername(username);
javaMailSender.setPassword(password);
javaMailSender.setJavaMailProperties(getMailProperties());

return javaMailSender;
}

private Properties getMailProperties() {
Properties properties = new Properties();
properties.setProperty("mail.smtp.auth", auth);
properties.setProperty("mail.smtp.timeout", timeout);
properties.setProperty("mail.smtp.starttls.enable", starttls);
return properties;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
package io.perfume.api.common.config;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;

@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
public class RedisConfiguration {}
public class RedisConfiguration {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private int port;

@Value("${spring.redis.password}")
private String password;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(password);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ public ResponseEntity<Void> checkUsername(@RequestBody @Valid CheckUsernameReque
@PostMapping("/email-verify/confirm")
public ResponseEntity<EmailVerifyConfirmResponseDto> confirmEmail(
@RequestBody @Valid EmailVerifyConfirmRequestDto dto) {
LocalDateTime now = LocalDateTime.now();
ConfirmEmailVerifyResult result =
createUserUseCase.confirmEmailVerify(dto.key(), dto.code(), now);
ConfirmEmailVerifyResult result = createUserUseCase.confirmEmailVerify(dto.email(), dto.code());

return ResponseEntity.ok(
new EmailVerifyConfirmResponseDto(result.email(), result.verifiedAt()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
import jakarta.validation.constraints.NotEmpty;

public record EmailVerifyConfirmRequestDto(
@NotEmpty @NotBlank String code, @NotEmpty @NotBlank String key) {}
@NotEmpty @NotBlank String email, @NotEmpty @NotBlank String code) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.perfume.api.user.adapter.out.persistence.emailcode;

import io.perfume.api.base.CustomHttpException;
import io.perfume.api.base.LogLevel;
import org.springframework.http.HttpStatus;

public class CodeNotFoundException extends CustomHttpException {
public CodeNotFoundException(String message) {
super(HttpStatus.BAD_REQUEST, message, message, LogLevel.INFO);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.perfume.api.user.adapter.out.persistence.emailcode;

import io.perfume.api.base.PersistenceAdapter;
import io.perfume.api.user.application.port.out.EmailCodeRepository;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;

@PersistenceAdapter
@RequiredArgsConstructor
public class EmailCodeAdapter implements EmailCodeRepository {

private final StringRedisTemplate stringRedisTemplate;

@Override
public void save(String email, String code, Duration duration) {
stringRedisTemplate.opsForValue().set(email, code, duration);
}

@Override
public void verify(String email, String code) {
String savedCode = stringRedisTemplate.opsForValue().get(email);
if (savedCode == null) {
throw new CodeNotFoundException("이메일에 대한 인증 코드가 존재하지 않습니다. email : " + email);
}
if (!code.equals(savedCode)) {
throw new CodeNotFoundException("이메일 인증 코드가 일치하지 않습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface CreateUserUseCase {

boolean validDuplicateUsername(String username);

ConfirmEmailVerifyResult confirmEmailVerify(String code, String key, LocalDateTime now);
ConfirmEmailVerifyResult confirmEmailVerify(String email, String code);

SendVerificationCodeResult sendEmailVerifyCode(SendVerificationCodeCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.perfume.api.user.application.port.out;

import java.time.Duration;

public interface EmailCodeRepository {
void save(String email, String code, Duration duration);

void verify(String email, String code);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

import io.perfume.api.auth.application.port.in.CheckEmailCertificateUseCase;
import io.perfume.api.auth.application.port.in.CreateVerificationCodeUseCase;
import io.perfume.api.auth.application.port.in.dto.CheckEmailCertificateCommand;
import io.perfume.api.auth.application.port.in.dto.CheckEmailCertificateResult;
import io.perfume.api.auth.application.port.in.dto.CreateVerificationCodeCommand;
import io.perfume.api.auth.application.port.in.dto.CreateVerificationCodeResult;
import io.perfume.api.user.application.exception.FailedRegisterException;
import io.perfume.api.user.application.exception.UserConflictException;
import io.perfume.api.user.application.port.in.CreateUserUseCase;
Expand All @@ -15,12 +11,15 @@
import io.perfume.api.user.application.port.in.dto.SignUpGeneralUserCommand;
import io.perfume.api.user.application.port.in.dto.SignUpSocialUserCommand;
import io.perfume.api.user.application.port.in.dto.UserResult;
import io.perfume.api.user.application.port.out.EmailCodeRepository;
import io.perfume.api.user.application.port.out.SocialAccountRepository;
import io.perfume.api.user.application.port.out.UserQueryRepository;
import io.perfume.api.user.application.port.out.UserRepository;
import io.perfume.api.user.domain.SocialAccount;
import io.perfume.api.user.domain.User;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import mailer.MailSender;
import org.slf4j.Logger;
Expand All @@ -35,10 +34,13 @@
public class RegisterService implements CreateUserUseCase {

private static final Logger logger = LoggerFactory.getLogger(RegisterService.class);
private static final Duration duration = Duration.ofMinutes(10);

private final UserRepository userRepository;
private final UserQueryRepository userQueryRepository;
private final SocialAccountRepository oauthRepository;
private final EmailCodeRepository emailCodeRepository;

private final CheckEmailCertificateUseCase checkEmailCertificateUseCase;
private final CreateVerificationCodeUseCase createVerificationCodeUseCase;
private final MailSender mailSender;
Expand Down Expand Up @@ -87,33 +89,29 @@ public boolean validDuplicateUsername(String username) {
}

@Override
public ConfirmEmailVerifyResult confirmEmailVerify(String code, String key, LocalDateTime now) {
logger.info("confirmEmailVerify code = {}, key = {}, now = {}", code, key, now);

CheckEmailCertificateCommand command = new CheckEmailCertificateCommand(code, key, now);
CheckEmailCertificateResult result =
checkEmailCertificateUseCase.checkEmailCertificate(command);

return new ConfirmEmailVerifyResult(result.email(), now);
public ConfirmEmailVerifyResult confirmEmailVerify(String email, String code) {
emailCodeRepository.verify(email, code);
LocalDateTime now = LocalDateTime.now();
return new ConfirmEmailVerifyResult(email, now);
}

@Override
public SendVerificationCodeResult sendEmailVerifyCode(SendVerificationCodeCommand command) {
CreateVerificationCodeCommand createVerificationCodeCommand =
new CreateVerificationCodeCommand(command.email(), command.now());
CreateVerificationCodeResult result =
createVerificationCodeUseCase.createVerificationCode(createVerificationCodeCommand);
// code 생성을 UUID로 대체하여 주석 처리했습니다.
// CreateVerificationCodeCommand createVerificationCodeCommand =
// new CreateVerificationCodeCommand(command.email(), command.now());
// CreateVerificationCodeResult result =
// createVerificationCodeUseCase.createVerificationCode(createVerificationCodeCommand);
String code = UUID.randomUUID().toString().substring(0, 6);

LocalDateTime sentAt = mailSender.send(command.email(), "이메일 인증을 완료해주세요.", result.code());
LocalDateTime sentAt = mailSender.send(command.email(), "Read A Perfume 이메일을 인증해 주세요.", code);

logger.info(
"sendEmailVerifyCode email = {}, code = {}, key = {}, now = {}",
command.email(),
result.code(),
result.signKey(),
sentAt);
"sendEmailVerifyCode email = {}, code = {}, now = {}", command.email(), code, sentAt);

emailCodeRepository.save(command.email(), code, duration);

return new SendVerificationCodeResult(result.signKey(), sentAt);
return new SendVerificationCodeResult(code, sentAt);
}

private User getUserByEmailOrCreateNew(SignUpSocialUserCommand command, LocalDateTime now) {
Expand Down
16 changes: 16 additions & 0 deletions perfume-api/src/main/resources/application-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ spring:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_AUTH_PASSWORD}
data:
web:
pageable:
Expand All @@ -35,6 +39,18 @@ spring:
scope:
- profile
- email
mail:
host: 'smtp.gmail.com'
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
timeout: 5000
starttls:
enable: true

whitelist:
cors:
Expand Down
23 changes: 19 additions & 4 deletions perfume-api/src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ spring:
pool-name: 'perfume-db-pool'
jpa:
hibernate:
ddl-auto: create-drop
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
redis:
host: localhost
port: 6379
password: very-strong-local-redis-password!@#$!%!@#
data:
web:
pageable:
Expand All @@ -31,12 +35,23 @@ spring:
client:
registration:
google:
client-id: ''
client-secret: ''
client-id: 'test'
client-secret: 'test'
scope:
- profile
- email

mail:
host: 'smtp.gmail.com'
port: 587
username: ''
password: ''
properties:
mail:
smtp:
auth: true
timeout: 5000
starttls:
enable: true
whitelist:
cors:
- http://localhost:3000
Expand Down
17 changes: 17 additions & 0 deletions perfume-api/src/main/resources/application-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,28 @@ spring:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect

redis:
host: localhost
port: 6379
password: ''
data:
web:
pageable:
one-indexed-parameters: true

mail:
host: 'smtp.gmail.com'
port: 587
username: ''
password: ''
properties:
mail:
smtp:
auth: true
timeout: 5000
starttls:
enable: true

whitelist:
cors:

Expand Down
Loading