Skip to content

Commit

Permalink
Merge pull request #163 from IT-Cotato/feature/162-implement-email-fu…
Browse files Browse the repository at this point in the history
…nctions

Feature: 이메일 관련 기능 구현(#162)
  • Loading branch information
yooooonshine authored Jan 16, 2025
2 parents 2e6b198 + 3f63791 commit de3485b
Show file tree
Hide file tree
Showing 25 changed files with 752 additions and 30 deletions.
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ dependencies {
implementation group: 'com.github.vladimir-bukhtoyarov', name: 'bucket4j-core', version: '8.0.1'
//aop
implementation 'org.springframework.boot:spring-boot-starter-aop'
//email
implementation 'org.springframework.boot:spring-boot-starter-mail'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package middle_point_search.backend;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import jakarta.annotation.PostConstruct;
import middle_point_search.backend.common.filter.RateLimitFilter;
import java.util.TimeZone;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.TimeZone;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import jakarta.annotation.PostConstruct;
import middle_point_search.backend.common.filter.RateLimitFilter;

@EnableAsync
@SpringBootApplication
@EnableJpaAuditing
@EnableAspectJAutoProxy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package middle_point_search.backend.common.email;

import java.util.Properties;

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 lombok.RequiredArgsConstructor;
import middle_point_search.backend.common.properties.EmailProperties;

@Configuration
@RequiredArgsConstructor
public class EmailConfig {

private final EmailProperties emailProperties;

@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(emailProperties.getHost());
mailSender.setPort(emailProperties.getPort());
mailSender.setUsername(emailProperties.getUsername());
mailSender.setPassword(emailProperties.getPassword());
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());

return mailSender;
}

private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", true);
properties.put("mail.smtp.starttls.enable", true);
properties.put("mail.smtp.starttls.required", true);
properties.put("mail.smtp.connectiontimeout", emailProperties.getConnectionTimeout());
properties.put("mail.smtp.timeout", emailProperties.getTimeout());
properties.put("mail.smtp.writetimeout", emailProperties.getWriteTimeout());

return properties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public enum UserErrorCode implements ErrorCode {
MEMBER_CREDENTIAL_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "M-002", "비밀번호가 일치하지 않습니다"),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M-201", "존재하지 않는 회원입니다."),
DUPLICATE_MEMBER_EMAIL(HttpStatus.CONFLICT, "M-001", "이미 존재하는 이메일입니다."),
REQUIRE_VERIFICATION_REQUEST_FIRST(HttpStatus.FORBIDDEN, "M-003", "이메일 인증을 먼저 진행해주세요."),
VERIFICATION_CODE_NOT_MATCH(HttpStatus.FORBIDDEN, "M-004", "인증 코드가 일치하지 않습니다."),
PASSWORD_NOT_MATCH(HttpStatus.FORBIDDEN, "M-005", "비밀번호가 일치하지 않습니다."),

//방 관련
ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "R-201", "존재하지 않는 방입니다."),
Expand Down Expand Up @@ -48,6 +51,9 @@ public enum UserErrorCode implements ErrorCode {

//서버 관련
API_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-001", "API 서버에 문제가 발생하였습니다."),

//이메일 관련
EMAIL_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "E-001", "이메일 전송에 실패하였습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package middle_point_search.backend.common.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@ConfigurationProperties(prefix = "mail")
public class EmailProperties {

private String host;
private int port;
private String username;
private String password;
private int connectionTimeout;
private int timeout;
private int writeTimeout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import middle_point_search.backend.common.properties.CorsProperties;
import middle_point_search.backend.common.properties.EmailProperties;
import middle_point_search.backend.common.properties.GoogleProperties;
import middle_point_search.backend.common.properties.JwtProperties;
import middle_point_search.backend.common.properties.KakaoProperties;
import middle_point_search.backend.common.properties.MarketProperties;
import middle_point_search.backend.common.properties.RedisProperties;
import middle_point_search.backend.common.properties.SecurityProperties;
import middle_point_search.backend.common.properties.CorsProperties;

// 전역적으로 사용되는 상수
@Configuration
Expand All @@ -20,7 +21,8 @@
MarketProperties.class,
KakaoProperties.class,
RedisProperties.class,
GoogleProperties.class
GoogleProperties.class,
EmailProperties.class
})
public class PropertyConfig {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package middle_point_search.backend.domains.email.domain;

import static lombok.AccessLevel.*;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = PROTECTED)
@RedisHash(value = "passwordReissueVerificationCode", timeToLive = 10 * 60)
public class PasswordReissueVerificationCode {

@Id
private String email;

@Setter
private String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package middle_point_search.backend.domains.email.domain;

import static lombok.AccessLevel.*;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@AllArgsConstructor
@NoArgsConstructor(access = PROTECTED)
@RedisHash(value = "signupVerificationCode", timeToLive = 10 * 60)
public class SignupVerificationCode {

@Id
private String email;

@Setter
private String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package middle_point_search.backend.domains.email.repository;

import org.springframework.data.repository.CrudRepository;

import middle_point_search.backend.domains.email.domain.PasswordReissueVerificationCode;

public interface PasswordReissueVerificationCodeRepository extends CrudRepository<PasswordReissueVerificationCode, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package middle_point_search.backend.domains.email.repository;

import org.springframework.data.repository.CrudRepository;

import middle_point_search.backend.domains.email.domain.SignupVerificationCode;

public interface SignupVerificationCodeRepository extends CrudRepository<SignupVerificationCode, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package middle_point_search.backend.domains.email.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import middle_point_search.backend.domains.email.util.EmailUtil;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EmailService {

private final EmailUtil emailUtil;

private static final String EMAIL_VERIFICATION_TITLE = "SyncSpot 이메일 인증 코드입니다.";
private static final String EMAIL_NEW_PASSWORD_TITLE = "SyncSpot 임시 비밀번호입니다.";
private static final String EMAIL_VERIFICATION_NOTICE_TEXT = "인증 코드는 %s 입니다.";
private static final String EMAIL_NEW_PASSWORD_NOTICE_TEXT = "임시 비밀번호는 %s 입니다. 로그인 후 비밀번호를 변경해주세요.";

// 인증번호 이메일 보내기
public void sendVerificationCodeEmail(String email, String code) {
String text = String.format(EMAIL_VERIFICATION_NOTICE_TEXT, code);
emailUtil.sendEmail(email, EMAIL_VERIFICATION_TITLE, text);
}

// 비밀번호 재발급 인증번호 이메일 보내기
public void sendPasswordReissueVerificationCodeEmail(String email, String code) {
String text = String.format(EMAIL_VERIFICATION_NOTICE_TEXT, code);
emailUtil.sendEmail(email, EMAIL_VERIFICATION_TITLE, text);
}

// 새 비밀번호 이메일 보내기
public void sendNewPassword(String email, String newPassword) {
String text = String.format(EMAIL_NEW_PASSWORD_NOTICE_TEXT, newPassword);

emailUtil.sendEmail(email, EMAIL_NEW_PASSWORD_TITLE, text);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package middle_point_search.backend.domains.email.service;

import static middle_point_search.backend.common.exception.errorCode.UserErrorCode.*;

import java.util.Random;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import middle_point_search.backend.common.exception.CustomException;
import middle_point_search.backend.domains.email.domain.PasswordReissueVerificationCode;
import middle_point_search.backend.domains.email.repository.PasswordReissueVerificationCodeRepository;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PasswordReissueVerificationCodeService {

private final PasswordReissueVerificationCodeRepository passwordReissueVerificationCodeRepository;

@Transactional
public void checkEmailCodeDuplicationAndSaveEmailCode(String email, String code) {
PasswordReissueVerificationCode passwordReissueVerificationCode = passwordReissueVerificationCodeRepository
.findById(email)
.orElse(null);

// 1. 이미 저장된 emailCode가 있으면 code만 변경
// 2. 없으면 새로 생성
if (passwordReissueVerificationCode != null) {
passwordReissueVerificationCode.setCode(code);
} else {
passwordReissueVerificationCode = new PasswordReissueVerificationCode(email, code);
}

passwordReissueVerificationCodeRepository.save(passwordReissueVerificationCode);
}

// 인증 코드 생성
public String createVerificationCode() {
Random random = new Random();

return String.format("%06d", random.nextInt(1000000)); // 000000부터 999999까지의 문자열 생성
}

// 인증 코드 확인
public void verifyEmailCode(String email, String code) throws CustomException {
PasswordReissueVerificationCode passwordReissueVerificationCode = passwordReissueVerificationCodeRepository.findById(
email).orElse(null);

if (passwordReissueVerificationCode == null) {
throw CustomException.from(REQUIRE_VERIFICATION_REQUEST_FIRST);
}

if (!passwordReissueVerificationCode.getCode().equals(code)) {
throw CustomException.from(VERIFICATION_CODE_NOT_MATCH);
}

passwordReissueVerificationCodeRepository.delete(passwordReissueVerificationCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package middle_point_search.backend.domains.email.service;

import static middle_point_search.backend.common.exception.errorCode.UserErrorCode.*;

import java.util.Random;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import middle_point_search.backend.common.exception.CustomException;
import middle_point_search.backend.domains.email.domain.SignupVerificationCode;
import middle_point_search.backend.domains.email.repository.SignupVerificationCodeRepository;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SignupVerificationCodeService {

private final SignupVerificationCodeRepository signupVerificationCodeRepository;

@Transactional
public void checkEmailCodeDuplicationAndSaveEmailCode(String email, String code) {
SignupVerificationCode signupVerificationCode = signupVerificationCodeRepository.findById(email).orElse(null);

// 1. 이미 저장된 emailCode가 있으면 code만 변경
// 2. 없으면 새로 생성
if (signupVerificationCode != null) {
signupVerificationCode.setCode(code);
} else {
signupVerificationCode = new SignupVerificationCode(email, code);
}

signupVerificationCodeRepository.save(signupVerificationCode);
}

// 인증 코드 생성
public String createVerificationCode() {
Random random = new Random();

return String.format("%06d", random.nextInt(1000000)); // 000000부터 999999까지의 문자열 생성
}

// 인증 코드 확인
public boolean verifyEmailCode(String email, String code) throws CustomException {
SignupVerificationCode signupVerificationCode = signupVerificationCodeRepository.findById(email).orElse(null);

if (signupVerificationCode == null) {
throw CustomException.from(REQUIRE_VERIFICATION_REQUEST_FIRST);
}

return signupVerificationCode.getCode().equals(code);
}

// 인증 코드 판별 후 삭제
@Transactional
public void validateEmailCodeAndDelete(String email, String code) {
if (!verifyEmailCode(email, code)) {
throw CustomException.from(VERIFICATION_CODE_NOT_MATCH);
}

signupVerificationCodeRepository.deleteById(email);
}
}
Loading

0 comments on commit de3485b

Please sign in to comment.