Skip to content
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
23 changes: 23 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,26 @@ include::{snippetsDir}/emailDuplicationCheck/1/http-response.adoc[]
include::{snippetsDir}/emailDuplicationCheck/1/response-fields.adoc[]

---


=== **2. 이메일 인증 api**

이메일 인증용 μ½”λ“œλ₯Ό λ°œμ†‘ν•˜λŠ” apiμž…λ‹ˆλ‹€.

==== Request
include::{snippetsDir}/emailAuthentication/1/http-request.adoc[]

==== Request Body Fields
include::{snippetsDir}/emailAuthentication/1/request-fields.adoc[]

==== 성곡 Response
include::{snippetsDir}/emailAuthentication/1/http-response.adoc[]

==== Response Body Fields
include::{snippetsDir}/emailAuthentication/1/response-fields.adoc[]

==== μ‹€νŒ¨ Response
μ‹€νŒ¨1.
include::{snippetsDir}/emailAuthentication/2/http-response.adoc[]
μ‹€νŒ¨ 2
include::{snippetsDir}/emailAuthentication/3/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.ftm.server.adapter.controller.user;

import com.ftm.server.adapter.dto.request.EmailAuthenticationRequest;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.domain.dto.command.EmailAuthenticationCommand;
import com.ftm.server.domain.usecase.user.EmailAuthenticationUseCase;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class EmailAuthenticationController {

private final EmailAuthenticationUseCase emailAuthenticationUseCase;

@PostMapping("/api/users/email/authentication")
public ResponseEntity<ApiResponse<Object>> emailAuthenticationCodeSender(
@Valid @RequestBody EmailAuthenticationRequest request) {
emailAuthenticationUseCase.sendEmailAuthenticationCode(
EmailAuthenticationCommand.from(request)); // command 객체 전달
return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.success(SuccessResponseCode.OK));
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.adapter.dto.request;

import jakarta.validation.constraints.Pattern;
import lombok.Data;

@Data
public class EmailAuthenticationRequest {

@Pattern(
regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$",
message = "이메일 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
private final String email;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ftm.server.adapter.gateway;

public interface MailSenderGateway {

void sendEmail(String to, String code);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.adapter.gateway.repository;

import com.ftm.server.entity.entities.EmailVerificationLogs;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EmailVerificationLogsRepository
extends JpaRepository<EmailVerificationLogs, Long> {

Optional<EmailVerificationLogs> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ public enum ErrorResponseCode {
// 409번
USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "E409_001", "이미 μ‘΄μž¬ν•˜λŠ” μ‚¬μš©μžμž…λ‹ˆλ‹€."),
PASSWORD_NOT_MATCHED(HttpStatus.CONFLICT, "E409_002", "λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."),
EXCEED_NUMBER_OF_TRIAL(HttpStatus.CONFLICT, "E409_003", "μ‹œλ„ κ°€λŠ₯ 횟수λ₯Ό μ΄ˆκ³Όν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ 후에 λ‹€μ‹œ μ‹œλ„ ν•΄ μ£Όμ„Έμš”."),

// 500번
UNKNOWN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E500_001", "μ•Œ 수 μ—†λŠ” μ„œλ²„ μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.");
UNKNOWN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E500_001", "μ•Œ 수 μ—†λŠ” μ„œλ²„ μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."),
FAIL_TO_SEND_EMAIL(HttpStatus.INTERNAL_SERVER_ERROR, "E500_002", "μ„œλ²„ λ‚΄λΆ€ 문제둜 메일 전솑에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Empty file.
22 changes: 22 additions & 0 deletions src/main/java/com/ftm/server/common/utils/RandomCodeCreator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ftm.server.common.utils;

import java.security.SecureRandom;
import org.springframework.stereotype.Component;

@Component
public class RandomCodeCreator {

private static final int CODE_LENGTH = 6;

private final SecureRandom random = new SecureRandom();

public String generateAuthCode() {

StringBuilder sb = new StringBuilder(CODE_LENGTH);
for (int i = 0; i < CODE_LENGTH; i++) {
int digit = random.nextInt(10);
sb.append(digit);
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.domain.dto.command;

import com.ftm.server.adapter.dto.request.EmailAuthenticationRequest;
import lombok.Data;

@Data
public class EmailAuthenticationCommand {
private final String email;

public static EmailAuthenticationCommand from(EmailAuthenticationRequest request) {
return new EmailAuthenticationCommand(request.getEmail());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.domain.dto.command;

import lombok.Data;

@Data
public class EmailVerificationLogCreationCommand {
private final String email;
private final String verificationCode;

public static EmailVerificationLogCreationCommand of(String email, String verificationCode) {
return new EmailVerificationLogCreationCommand(email, verificationCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.ftm.server.domain.service;

import com.ftm.server.adapter.gateway.repository.EmailVerificationLogsRepository;
import com.ftm.server.domain.dto.command.EmailVerificationLogCreationCommand;
import com.ftm.server.domain.dto.query.FindByEmailQuery;
import com.ftm.server.entity.entities.EmailVerificationLogs;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class EmailVerificationLogsService {
private final EmailVerificationLogsRepository emailVerificationLogsRepository;

public Optional<EmailVerificationLogs> findEmailVerificationLogsByEmail(
FindByEmailQuery query) {
return emailVerificationLogsRepository.findByEmail(query.getEmail());
}

public void saveEmailVerificationLogs(EmailVerificationLogCreationCommand command) {
emailVerificationLogsRepository.save(EmailVerificationLogs.from(command));
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller -> UseCase κ³„μΈ΅μœΌλ‘œ 전달될 λ•Œ Command or Query 객체둜 μ „λ‹¬ν•΄μ£ΌκΈ°λ‘œ ν–ˆλŠ”λ° EmailAuthenticationUseCase sendEmailAuthenticationCode() λ©”μ„œλ“œμ—μ„œ Request DTOλ₯Ό λ°›κ³  μžˆμŠ΅λ‹ˆλ‹€ !

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.ftm.server.domain.usecase.user;

import com.ftm.server.adapter.gateway.MailSenderGateway;
import com.ftm.server.common.annotation.UseCase;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import com.ftm.server.common.utils.RandomCodeCreator;
import com.ftm.server.domain.dto.command.EmailAuthenticationCommand;
import com.ftm.server.domain.dto.command.EmailVerificationLogCreationCommand;
import com.ftm.server.domain.dto.query.FindByEmailQuery;
import com.ftm.server.domain.service.EmailVerificationLogsService;
import com.ftm.server.entity.entities.EmailVerificationLogs;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
import lombok.RequiredArgsConstructor;

@UseCase
@RequiredArgsConstructor
public class EmailAuthenticationUseCase {

private final MailSenderGateway mailSenderGateway;

private final RandomCodeCreator randomCodeCreator;

private final EmailVerificationLogsService emailVerificationLogsService;

@Transactional
public void sendEmailAuthenticationCode(EmailAuthenticationCommand command) {

String authCode = randomCodeCreator.generateAuthCode();
String email = command.getEmail();

Optional<EmailVerificationLogs> emailVerificationLogs =
emailVerificationLogsService.findEmailVerificationLogsByEmail(
FindByEmailQuery.of(email));

int MAX_TRIALS = 5;
int BLOCK_MINUTES = 15;

if (emailVerificationLogs.isEmpty()) { // 이메일 인증을 ν•œλ²ˆλ„ μ‹œλ„ν•˜μ§€ μ•Šμ€ 경우 log μƒˆλ‘œ 생성
emailVerificationLogsService.saveEmailVerificationLogs(
EmailVerificationLogCreationCommand.of(email, authCode));
} else if (emailVerificationLogs.get().getTrialNum()
< MAX_TRIALS) { // 이메일 인증 μ‹œλ„λ₯Ό ν•œμ  μžˆμœΌλ‚˜ μ‹œλ„ 횟수λ₯Ό μ΄ˆκ³Όν•˜μ§€ μ•Šμ€ 경우 log update
emailVerificationLogs.get().updateVerificationStatus(authCode);
} else if (emailVerificationLogs // 이메일 인증 μ‹œλ„ 횟수λ₯Ό μ΄ˆκ³Όν–ˆμœΌλ‚˜ λ§ˆμ§€λ§‰ μ‹œλ„ νšŸμˆ˜λ‘œλΆ€ν„° 15λΆ„ 이상이 μ§€λ‚œ 경우 log μ΄ˆκΈ°ν™”
.get()
.getTokenIssuanceTime()
.isBefore(LocalDateTime.now().minusMinutes(BLOCK_MINUTES))) {
emailVerificationLogs.get().initializeVerificationStatus(authCode);
} else { // 이메일 인증 μ‹œλ„ 횟수λ₯Ό λ‹¨μˆœνžˆ μ΄ˆκ³Όν•œ 경우
throw new CustomException(ErrorResponseCode.EXCEED_NUMBER_OF_TRIAL);
}
mailSenderGateway.sendEmail(email, authCode);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ftm.server.entity.entities;

import com.ftm.server.domain.dto.command.EmailVerificationLogCreationCommand;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import lombok.AccessLevel;
Expand All @@ -19,7 +20,6 @@ public class EmailVerificationLogs extends BaseEntity {
@Column(nullable = false)
private String email;

@Lob
@Column(name = "verification_code", nullable = false)
private String verificationCode;

Expand All @@ -45,4 +45,26 @@ private EmailVerificationLogs(
this.trialNum = trialNum;
this.tokenIssuanceTime = tokenIssuanceTime;
}

public static EmailVerificationLogs from(EmailVerificationLogCreationCommand command) {
return EmailVerificationLogs.builder()
.email(command.getEmail())
.verificationCode(command.getVerificationCode())
.isVerified(false)
.trialNum(1)
.tokenIssuanceTime(LocalDateTime.now())
.build();
}

public void updateVerificationStatus(String verificationCode) {
this.trialNum++;
this.verificationCode = verificationCode;
this.tokenIssuanceTime = LocalDateTime.now();
}

public void initializeVerificationStatus(String verificationCode) {
this.trialNum = 1;
this.verificationCode = verificationCode;
this.tokenIssuanceTime = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ public class SecurityConfig {
"https://dev-api.fittheman.site", // 개발 ν™˜κ²½ μ„œλ²„ 도메인
"https://fittheman.site"); // 개발 ν™˜κ²½ ν΄λΌμ΄μ–ΈνŠΈ 도메인

private static final String[] ANONYMOUS_MATCHERS = {"/docs/**", "/api/users/email/duplication"};
private static final String[] GET_ANONYMOUS_MATCHERS = {"/api/users/email/duplication"};

private static final String[] POST_ANONYMOUS_MATCHERS = {"/api/users/email/authentication"};

private static final String[] ANONYMOUS_MATCHERS = {"/docs/**"};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand Down Expand Up @@ -75,8 +79,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authorizeHttpRequests(
authorize -> {
authorize
// 정적 λ¦¬μ†ŒμŠ€ 경둜 ν—ˆμš©
.requestMatchers(ANONYMOUS_MATCHERS)
.permitAll() // 정적 λ¦¬μ†ŒμŠ€ 경둜 ν—ˆμš©
.requestMatchers(HttpMethod.GET, GET_ANONYMOUS_MATCHERS)
.permitAll()
.requestMatchers(HttpMethod.POST, POST_ANONYMOUS_MATCHERS)
.permitAll();

// TODO: μš”μ²­ ν—ˆμš© νŠΉμ • API μΆ”κ°€ (νšŒμ›κ°€μž…, 둜그인 λ“±)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.ftm.server.infrastructure.smtp;

import java.util.Properties;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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
@EnableConfigurationProperties(MailProperties.class)
public class MailSenderConfig {

private final MailProperties mailProperties;

public MailSenderConfig(MailProperties mailProperties) {
this.mailProperties = mailProperties;
}

@Bean
public JavaMailSender javaMailService() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost(mailProperties.getHost());
javaMailSender.setUsername(mailProperties.getUsername());
javaMailSender.setPassword(mailProperties.getPassword());
javaMailSender.setPort(mailProperties.getPort());

Properties properties = javaMailSender.getJavaMailProperties();
properties.put("mail.smtp.auth", "true");
properties.put("mail.smtp.starttls.enable", "true");

javaMailSender.setJavaMailProperties(properties);
javaMailSender.setDefaultEncoding("UTF-8");

return javaMailSender;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.ftm.server.infrastructure.smtp;

import com.ftm.server.adapter.gateway.MailSenderGateway;
import com.ftm.server.common.annotation.InfraService;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

@RequiredArgsConstructor
@InfraService
public class MailSenderService implements MailSenderGateway {

private final JavaMailSender mailSender;

@Override
public void sendEmail(String to, String code) {
MimeMessage message = mailSender.createMimeMessage();

String mailContent =
"""
<html>
<body style="text-align:center; font-family:Arial, sans-serif;">
<h1>핏더맨 이메일 인증 μ½”λ“œμž…λ‹ˆλ‹€</h1>
<div style="font-size:24px; margin:20px 0; color:#4CAF50;"><strong>%s</strong></div>
<p>μœ„ 인증 μ½”λ“œλ₯Ό μž…λ ₯ν•˜μ—¬ 이메일 인증을 μ™„λ£Œν•΄ μ£Όμ„Έμš”.</p>
<br/>
<hr/>
<p style="font-size:12px; color:#888;">이 메일은 핏더맨 μ‹œμŠ€ν…œμ— μ˜ν•΄ μžλ™ λ°œμ†‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.</p>
<p style="font-size:12px; color:#888;">문의:ftmanserver@gmail.com</p>
</body>
</html>
"""
.formatted(code);
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(to);
helper.setSubject("[핏더맨] 이메일 인증을 μ™„λ£Œν•΄μ£Όμ„Έμš”!");
helper.setText(mailContent, true);
mailSender.send(message);
} catch (MailException | MessagingException e) {
throw new CustomException((ErrorResponseCode.FAIL_TO_SEND_EMAIL), e.getMessage());
}
}
}
Loading