Skip to content

Commit

Permalink
Merge pull request #17 from PawWithU/feat/16-email-confirm-api
Browse files Browse the repository at this point in the history
[Feature] 이메일 인증번호 발송 API 구현
  • Loading branch information
kyeong-hyeok authored Oct 28, 2023
2 parents 2812f46 + dd36619 commit 379c5dc
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 5 deletions.
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
// mail
implementation 'org.springframework.boot:spring-boot-starter-mail'
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.pawwithu.connectdog;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
Expand All @@ -8,6 +10,7 @@
@EnableJpaAuditing
@EnableCaching
@SpringBootApplication
@OpenAPIDefinition(servers = {@Server(url = "/", description = "https://dev-api.connectdog.site")})
public class ConnectdogApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospe
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(request ->
request.requestMatchers(mvcMatcherBuilder.pattern("/login")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/api/sign-up")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/sign-up/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/h2-console/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/css/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/js/**")).permitAll()
Expand All @@ -45,8 +45,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospe
.requestMatchers(mvcMatcherBuilder.pattern("/swagger-ui/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/swagger-resources/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/v3/api-docs/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/api/sign-up/email")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/api/members/userName/isDuplicated")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/members/userName/isDuplicated")).permitAll()
.anyRequest().authenticated());

return http.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.pawwithu.connectdog.domain.auth.controller;

import com.pawwithu.connectdog.domain.auth.dto.request.EmailRequest;
import com.pawwithu.connectdog.domain.auth.dto.request.VolunteerSignUpRequest;
import com.pawwithu.connectdog.domain.auth.dto.response.EmailResponse;
import com.pawwithu.connectdog.domain.auth.service.AuthService;
import com.pawwithu.connectdog.domain.auth.service.EmailService;
import com.pawwithu.connectdog.error.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -18,10 +22,11 @@
@Tag(name = "Sign-Up", description = "Sign-Up API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/sign-up")
@RequestMapping("/sign-up")
public class SignUpController {

private final AuthService authService;
private final EmailService emailService;

@Operation(summary = "자체 회원가입", description = "이메일을 사용해 회원가입을 합니다.",
responses = {@ApiResponse(responseCode = "204", description = "자체 회원가입 성공")
Expand All @@ -35,4 +40,16 @@ public ResponseEntity<Void> signUp(@RequestBody VolunteerSignUpRequest volunteer
return ResponseEntity.noContent().build();
}

@Operation(summary = "이메일 인증번호 전송", description = "입력한 이메일로 인증번호를 전송합니다.",
responses = {@ApiResponse(responseCode = "200", description = "이메일 인증번호 전송 성공")
, @ApiResponse(responseCode = "400"
, description = "V1, 이메일 형식에 맞지 않습니다. \t\n V1, 이메일은 필수 입력 값입니다. \t\n A1, 이미 존재하는 이메일입니다. \t\n A4, 이메일 인증 코드 전송을 실패했습니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping("/email")
public ResponseEntity<EmailResponse> mailConfirm(@RequestBody @Valid EmailRequest emailRequest){
EmailResponse emailResponse = emailService.sendEmail(emailRequest);
return ResponseEntity.ok(emailResponse);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pawwithu.connectdog.domain.auth.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record EmailRequest(@Email(message="이메일 형식에 맞지 않습니다.")
@NotBlank(message = "이메일은 필수 입력 값입니다.")String email) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.pawwithu.connectdog.domain.auth.dto.response;

public record EmailResponse(String authCode) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.pawwithu.connectdog.domain.auth.service;

import com.pawwithu.connectdog.domain.auth.dto.request.EmailRequest;
import com.pawwithu.connectdog.domain.auth.dto.response.EmailResponse;
import com.pawwithu.connectdog.domain.intermediary.repository.IntermediaryRepository;
import com.pawwithu.connectdog.domain.volunteer.repository.VolunteerRepository;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

import java.io.UnsupportedEncodingException;
import java.util.Random;

import static com.pawwithu.connectdog.error.ErrorCode.ALREADY_EXIST_EMAIL;
import static com.pawwithu.connectdog.error.ErrorCode.EMAIL_SEND_ERROR;

@Service
@RequiredArgsConstructor
public class EmailService {

private final JavaMailSender emailSender;
private final SpringTemplateEngine templateEngine;
private String authNum; //랜덤 인증 코드
private final VolunteerRepository volunteerRepository;
private final IntermediaryRepository intermediaryRepository;

/**
* 랜덤 인증 코드 생성
*/
private void createCode() {
Random random = new Random();
StringBuffer key = new StringBuffer();

for(int i = 0; i < 8; i++) { // 8자리
int index = random.nextInt(3);

switch (index) {
case 0 : // 소문자
key.append((char) ((int)random.nextInt(26) + 97));
break;
case 1: // 대문자
key.append((char) ((int)random.nextInt(26) + 65));
break;
case 2: // 0-9 숫자
key.append(random.nextInt(9));
break;
}
}
authNum = key.toString();
}

/**
* 메일 양식 작성
*/
private MimeMessage createEmailForm(String email) throws MessagingException, UnsupportedEncodingException {

createCode(); //인증 코드 생성
String setFrom = "[email protected]"; // email-config에 설정한 자신의 이메일 주소
String toEmail = email; // 받는 사람
String title = "ConnectDog\uD83D\uDC3E 회원가입 인증 번호입니다."; // 제목

MimeMessage message = emailSender.createMimeMessage();
message.addRecipients(MimeMessage.RecipientType.TO, toEmail); // 보낼 이메일 설정
message.setSubject(title);
message.setFrom(setFrom);
message.setText(setContext(authNum), "utf-8", "html");

return message;
}

/**
* 메일 전송
*/
public EmailResponse sendEmail(EmailRequest emailRequest) throws BadRequestException {
// 이메일 중복 검사
if (volunteerRepository.existsByEmail(emailRequest.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
if (intermediaryRepository.existsByEmail(emailRequest.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
try{
// 메일전송에 필요한 정보 설정
MimeMessage emailForm = createEmailForm(emailRequest.email());
emailSender.send(emailForm);
return new EmailResponse(authNum);
}catch (UnsupportedEncodingException | MessagingException e){
throw new BadRequestException(EMAIL_SEND_ERROR);
}

}

/**
* context 설정
*/
private String setContext(String code) {
Context context = new Context();
context.setVariable("code", code);
return templateEngine.process("mail", context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class Volunteer {
private String password; // 비밀번호
@Column(length = 15, nullable = false)
private String nickname; // 닉네임
@Column(length = 10, nullable = false)
@Column(length = 10)
private String name; // 이름
private String phone; // 이동봉사자 휴대폰 번호
@Enumerated(EnumType.STRING)
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/pawwithu/connectdog/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum ErrorCode {
ALREADY_EXIST_EMAIL("A1", "이미 존재하는 이메일입니다."),
ALREADY_EXIST_NICKNAME("A2", "이미 존재하는 사용자 닉네임입니다."),
ALREADY_LOGOUT_MEMBER("A3", "이미 로그아웃한 회원입니다"),
EMAIL_SEND_ERROR("A4", "이메일 인증 코드 전송을 실패했습니다."),

TOKEN_NOT_EXIST("T1", "토큰이 존재하지 않습니다."),
TOKEN_IS_EXPIRED("T2", "만료된 토큰입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.List;
import java.util.stream.Collectors;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {
Expand All @@ -31,5 +38,17 @@ public ResponseEntity<ErrorResponse> handle(final TokenException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(e.getCode(), e.getMessage()));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
// 에러 반환이 여러 개일 경우 첫 번째 에러를 반환
String firstErrorMessage = bindingResult.getFieldErrors().get(0).getDefaultMessage();
// 모든 에러 메시지는 로그로 남김
List<String> errorList = bindingResult.getFieldErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.toList());
log.warn("MethodArgumentNotValidExceptionException = {}", errorList);

return ResponseEntity.status(BAD_REQUEST).body(ErrorResponse.of("V1", firstErrorMessage));
}


}
19 changes: 19 additions & 0 deletions src/main/resources/templates/mail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<body>
<div style="margin:100px;">
<h1> 안녕하세요. ConnectDog🐾입니다.</h1>
<br>
<p> 아래 코드를 회원가입 창으로 돌아가 입력해주세요.</p>
<br>

<div align="center" style="border:1px solid black; font-family:verdana;">
<h3 style="color:blue"> 회원가입 인증 코드 입니다. </h3>
<div style="font-size:130%" th:text="${code}"> </div>
</div>
<br/>
</div>

</body>
</html>

0 comments on commit 379c5dc

Please sign in to comment.