Skip to content

Commit

Permalink
Merge pull request #1 from JangYouJung/feature/Me1tingPot#56
Browse files Browse the repository at this point in the history
[Feature] λ§ˆμ΄νŽ˜μ΄μ§€ ν”„λ‘œν•„ 사진 μΆ”κ°€/μ‚­μ œ κ΅¬ν˜„ 및 νšŒμ›κ°€μž… 이메일 인증 κ΅¬ν˜„
  • Loading branch information
JangYouJung authored Jun 10, 2024
2 parents b0aba05 + 55779b5 commit f1a3da8
Show file tree
Hide file tree
Showing 27 changed files with 637 additions and 42 deletions.
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ dependencies {
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
implementation 'org.springframework.security:spring-security-messaging'

// mail
implementation 'org.springframework.boot:spring-boot-starter-mail:3.0.5'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// JWT
// implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import meltingpot.server.auth.controller.dto.*;
import meltingpot.server.exception.AuthException;
import meltingpot.server.exception.DuplicateException;
import meltingpot.server.exception.InvalidTokenException;
import meltingpot.server.util.ResponseCode;
import meltingpot.server.util.ResponseData;
Expand All @@ -23,7 +25,7 @@
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("auth")
@RequestMapping("/api/v1/auth")
public class AuthController {

private final AuthService authService;
Expand All @@ -39,15 +41,20 @@ public class AuthController {
public ResponseEntity<ResponseData<AccountResponseDto>> signup(
@RequestBody @Valid SignupRequestDto request
){
AccountResponseDto data = authService.signup(request);
logger.info("SIGNUP_SUCCESS (200 OK) :: userId = {}, userEmail = {}",
data.getId(), data.getEmail());
return ResponseData.toResponseEntity(ResponseCode.SIGNUP_SUCCESS, data);
try{
AccountResponseDto data = authService.signup(request);
logger.info("SIGNUP_SUCCESS (200 OK) :: userId = {}, userEmail = {}",
data.getId(), data.getEmail());
return ResponseData.toResponseEntity(ResponseCode.SIGNUP_SUCCESS, data);

}catch ( AuthException e ){
return ResponseData.toResponseEntity( e.getResponseCode(), null);
}
}

// ν”„λ‘œν•„ 이미지 URL 생성
@GetMapping("/image-url")
@Operation(summary = "ν”„λ‘œν•„ 이미지 URL 생성", description = "ν”„λ‘œν•„ 이미지 μ—…λ‘œλ“œλ₯Ό μœ„ν•œ URL을 μƒμ„±ν•©λ‹ˆλ‹€. μƒμ„±λœ URL에 PUT으둜 이미지λ₯Ό μ—…λ‘œλ“œ ν•œ λ’€ keyλ₯Ό νšŒμ›κ°€μž…μ— μ²¨λΆ€ν•΄μ£Όμ„Έμš”.")
@Operation(summary = "νšŒμ›κ°€μž… ν”„λ‘œν•„ 이미지 URL 생성", description = "ν”„λ‘œν•„ 이미지 μ—…λ‘œλ“œλ₯Ό μœ„ν•œ URL을 μƒμ„±ν•©λ‹ˆλ‹€. μƒμ„±λœ URL에 PUT으둜 이미지λ₯Ό μ—…λ‘œλ“œ ν•œ λ’€ keyλ₯Ό νšŒμ›κ°€μž…μ— μ²¨λΆ€ν•΄μ£Όμ„Έμš”.")
@ApiResponses(value = {
@ApiResponse(responseCode = "OK", description = "ν”„λ‘œν•„ 이미지 URL 생성 성곡")
})
Expand Down Expand Up @@ -100,9 +107,6 @@ public ResponseEntity<ResponseData<ReissueTokenResponseDto>> reissueToken(
}



// 이메일 인증

// λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ •

// νƒˆν‡΄
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package meltingpot.server.auth.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import meltingpot.server.auth.controller.dto.MailVerificationRequestDto;
import meltingpot.server.auth.controller.dto.VerificationCodeRequestDto;
import meltingpot.server.auth.service.MailService;
import meltingpot.server.exception.DuplicateException;
import meltingpot.server.exception.MailVerificationException;
import meltingpot.server.util.ResponseCode;
import meltingpot.server.util.ResponseData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/mail")
public class MailController {
private final MailService mailService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

// μ •νšŒμ› 인증 메일 전솑
@PostMapping("")
@Operation(summary="이메일 인증번호 전솑 [μž„μ‹œ]", description="[이메일 인증 μž„μ‹œ κ΅¬ν˜„ SMTP]\n μž…λ ₯ 받은 μ΄λ©”μΌλ‘œ 인증 번호λ₯Ό μ „μ†‘ν•©λ‹ˆλ‹€.")
@ApiResponses(value = {
@ApiResponse(responseCode = "MAIL_VERIFICATION_SEND_SUCCESS", description = "이메일 인증번호 전솑 성곡"),
@ApiResponse(responseCode = "VERIFICATION_CODE_ALREADY_EXIST", description = "이미 μƒμ„±ν•œ 인증 λ²ˆν˜Έκ°€ μžˆμŠ΅λ‹ˆλ‹€"),
@ApiResponse(responseCode = "MAIL_SEND_FAIL", description = "이메일 전솑 μ‹€νŒ¨")
})
public ResponseEntity<ResponseData> sendVerificationMail(
@RequestBody @Valid MailVerificationRequestDto request
) {
try{
logger.info("MAIL_VERIFICATION_SEND_SUCCESS (200 OK)");
return ResponseData.toResponseEntity(mailService.sendVerificationMail(request));

}catch(MailVerificationException e){
return ResponseData.toResponseEntity(e.getResponseCode());
}
}

@PostMapping("verification")
@Operation(summary="이메일 인증번호 확인", description="이메일 인증 번호λ₯Ό μž…λ ₯ λ°›κ³  μ˜¬λ°”λ₯Έ λ²ˆν˜ΈμΈμ§€ ν™•μΈν•©λ‹ˆλ‹€." )
@ApiResponses(value = {
@ApiResponse(responseCode = "MAIL_VERIFICATION_CHECK_SUCCESS", description = "μΈμ¦λ²ˆν˜Έκ°€ μΌμΉ˜ν•©λ‹ˆλ‹€"),
@ApiResponse(responseCode = "AUTHENTICATION_NOT_FOUND", description = "메일 인증 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€"),
@ApiResponse(responseCode = "AUTH_TIME_OUT", description = "인증 μ‹œκ°„μ„ μ΄ˆκ³Όν–ˆμŠ΅λ‹ˆλ‹€"),
@ApiResponse(responseCode = "AUTH_NUMBER_INCORRECT", description = "인증 λ²ˆν˜Έκ°€ ν‹€λ ΈμŠ΅λ‹ˆλ‹€"),
})
public ResponseEntity<ResponseData> checkVerification(
@RequestBody @Valid VerificationCodeRequestDto request
) {
try{
logger.info("VERIFICATION_CHECK_SUCCESS (200 OK)");
return ResponseData.toResponseEntity( mailService.checkVerification(request));

}catch(MailVerificationException e){
return ResponseData.toResponseEntity(e.getResponseCode());
}
}

// 이메일 쀑볡 확인
@PostMapping("duplication")
@Operation(summary="이메일 쀑볡 체크", description="이미 κ°€μž…ν•œ 이메일인지 ν™•μΈν•˜λŠ” API μž…λ‹ˆλ‹€.\n" )
public ResponseEntity<ResponseData> checkEmail(
@RequestBody @Valid MailVerificationRequestDto request) {
try{
mailService.checkUserName(request.email());
return ResponseData.toResponseEntity(ResponseCode.MAIL_AVAILABLE);
}catch (DuplicateException e){
return ResponseData.toResponseEntity(ResponseCode.EMAIL_DUPLICATION);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package meltingpot.server.auth.controller.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import meltingpot.server.util.VerificationUtil;

public record MailVerificationRequestDto(
@NotBlank(message = "email is required")
@Pattern(regexp = VerificationUtil.USERNAME_REGEXP)
String email
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProfileImageSignupDto {
public class ProfileImageRequestDto {
String imageKey;
boolean thumbnail;
int sequence;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public record SignupRequestDto(
LocalDate birth,
String nationality,
List<String> languages,
List<ProfileImageSignupDto> profileImages
List<ProfileImageRequestDto> profileImages

){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package meltingpot.server.auth.controller.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import meltingpot.server.util.VerificationUtil;

public record VerificationCodeRequestDto(
@NotBlank(message = "email is required")
@Pattern(regexp = VerificationUtil.USERNAME_REGEXP)
String email,
@NotBlank
String code
) {
}
26 changes: 13 additions & 13 deletions src/main/java/meltingpot/server/auth/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
import meltingpot.server.domain.entity.AccountLanguage;
import meltingpot.server.domain.entity.AccountProfileImage;
import meltingpot.server.domain.entity.enums.Gender;
import meltingpot.server.exception.DuplicateException;
import meltingpot.server.exception.InvalidTokenException;
import meltingpot.server.exception.ResourceNotFoundException;
import meltingpot.server.domain.repository.MailVerificationRepository;
import meltingpot.server.exception.*;
import meltingpot.server.config.TokenProvider;
import meltingpot.server.domain.entity.RefreshToken;
import meltingpot.server.domain.entity.Account;
Expand Down Expand Up @@ -47,13 +46,22 @@ public class AuthService implements UserDetailsService {
private final RefreshTokenRepository refreshTokenRepository;
private final PasswordEncoder passwordEncoder;
private final FileService fileService;
private final MailVerificationRepository mailVerificationRepository;

// νšŒμ›κ°€μž…
@Transactional
public AccountResponseDto signup(SignupRequestDto signupRequest) {

// 이메일(아이디) 쀑볡 검사
checkUserName(signupRequest.email());
// TODO 개발 μ™„λ£Œ ν›„ 이메일 인증 확인 주석 ν’€κΈ°
// // 이메일 인증을 거친 μœ νš¨ν•œ 이메일인지 확인
// if(!mailVerificationRepository.existsByEmailAndVerifiedTrue(signupRequest.email())){
// throw new AuthException(ResponseCode.MAIL_NOT_AUTHORIZED);
// };

// 이미 κ°€μž…ν•œ 이메일인지 확인
if(accountRepository.existsByUsername(signupRequest.email())){
throw new AuthException(ResponseCode.EMAIL_DUPLICATION);
}

Account account = Account.builder()
.username(signupRequest.email())
Expand Down Expand Up @@ -160,14 +168,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx
return new AccountUser(account);
}

// νšŒμ›κ°€μž…μ‹œ 이메일 μœ νš¨μ„± 확인
@Transactional(readOnly = true)
public void checkUserName(String username) {
if(accountRepository.existsByUsername(username)){
throw new DuplicateException(ResponseCode.EMAIL_DUPLICATION);
}
}

@Transactional(rollbackFor = Exception.class)
public ReissueTokenResponseDto reissueToken( String oldAccessToken, String refreshToken ) {
TokenDto reissuedTokenDto;
Expand Down
121 changes: 121 additions & 0 deletions src/main/java/meltingpot/server/auth/service/MailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package meltingpot.server.auth.service;

import lombok.RequiredArgsConstructor;
import meltingpot.server.auth.controller.dto.MailVerificationRequestDto;
import meltingpot.server.auth.controller.dto.VerificationCodeRequestDto;
import meltingpot.server.domain.entity.Constants;
import meltingpot.server.domain.entity.MailVerification;
import meltingpot.server.domain.repository.AccountRepository;
import meltingpot.server.domain.repository.MailVerificationRepository;
import meltingpot.server.exception.DuplicateException;
import meltingpot.server.exception.MailVerificationException;
import meltingpot.server.util.MailUtil;
import meltingpot.server.util.ResponseCode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Random;

@RequiredArgsConstructor
@Service
public class MailService {
private final MailVerificationRepository mailVerificationRepository;
private final MailUtil mailUtil;
private final AccountRepository accountRepository;

// 인증 μ½”λ“œ 생성
private String createCode() {
try {
Random rand = SecureRandom.getInstanceStrong();
StringBuilder code = new StringBuilder();

for (int i = 0; i < 6; i++) {
// 0~9κΉŒμ§€ λ‚œμˆ˜ 생성
String num = Integer.toString(rand.nextInt(10));
code.append(num);
}

return code.toString();

} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}

// 인증 메일 λ°œμ†‘
@Transactional
public ResponseCode sendVerificationMail(MailVerificationRequestDto request) {
String email = request.email();

// 이미 μœ νš¨ν•œ 인증 정보가 μžˆλŠ” 경우
Optional<MailVerification> oldMailVerification = mailVerificationRepository.findByEmailAndExpiredAtIsAfterNowAndVerifiedFalse(email,LocalDateTime.now());
if(oldMailVerification.isPresent()){
throw new MailVerificationException(ResponseCode.VERIFICATION_CODE_ALREADY_EXIST);
}

String code = createCode();
Map<String,String> mailValues = Map.of("code", code);

String title = "[λ©œνŒ…νŒŸ] 이메일 인증을 μœ„ν•œ 인증 번호 μ•ˆλ‚΄";

// 메일 전솑
mailUtil.sendMimeMessageMailWithValues(title, email, "EmailAuthenticationForm.html", mailValues);

LocalDateTime expiredAt = LocalDateTime.now().plusMinutes(Constants.AUTH_TIME_LIMIT);

MailVerification mailVerification = MailVerification.builder()
.email(email)
.expiredAt(expiredAt)
.authenticationNumber(code)
.build();

mailVerificationRepository.save(mailVerification);

return ResponseCode.MAIL_VERIFICATION_SEND_SUCCESS;

}


// 인증 번호 확인
@Transactional
public ResponseCode checkVerification(VerificationCodeRequestDto request) {

// ν˜„μž¬ μœ νš¨ν•œ 인증 정보 κ°€μ Έμ˜€κΈ°
MailVerification mailVerification = mailVerificationRepository.findByEmailAndExpiredAtIsAfterNowAndVerifiedFalse(request.email(), LocalDateTime.now())
.orElseThrow( // 인증 정보가 μ—†λŠ” 경우
()->new MailVerificationException(ResponseCode.AUTHENTICATION_NOT_FOUND)
);

// 인증 번호 유효 기간을 μ΄ˆκ³Όν•œ 경우
if(LocalDateTime.now().isAfter(mailVerification.getExpiredAt())){
throw new MailVerificationException(ResponseCode.AUTH_TIME_OUT);
}

// 인증 λ²ˆν˜Έκ°€ ν‹€λ¦° 경우
if(!mailVerification.getAuthenticationNumber().equals(request.code())){
throw new MailVerificationException(ResponseCode.AUTH_NUMBER_INCORRECT);
}

// 인증에 μ„±κ³΅ν•œ 경우: μ œν•œ μ‹œκ°„ 내에 인증 번호λ₯Ό μ˜¬λ°”λ₯΄κ²Œ μž…λ ₯ν•œ 경우
mailVerification.setVerified(true);
mailVerificationRepository.save(mailVerification);

return ResponseCode.MAIL_VERIFICATION_CHECK_SUCCESS;

}

// νšŒμ›κ°€μž…μ‹œ 이메일 μœ νš¨μ„± 확인
@Transactional(readOnly = true)
public void checkUserName(String username) {
if(accountRepository.existsByUsername(username)){
throw new DuplicateException(ResponseCode.EMAIL_DUPLICATION);
}
}

}
2 changes: 1 addition & 1 deletion src/main/java/meltingpot/server/config/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
// 정상 토큰이면 ν•΄λ‹Ή ν† ν°μœΌλ‘œ Authentication 을 κ°€μ Έμ™€μ„œ SecurityContext 에 μ €μž₯
// κΆŒν•œμ΄ ν•„μš”ν•˜μ§€ μ•Šμ€ μš”μ²­μ€ custom jwt filterλ₯Ό κ±°μΉ˜μ§€ μ•Šλ„λ‘ μ„€μ •
if (request.getRequestURI().contains("/contract") || request.getRequestURI()
.contains("/auth") || request.getRequestURI().contains("/mail/reset-password")
.contains("/api/v1/auth") || request.getRequestURI().contains("/api/v1/mail")
|| request.getRequestURI().contains("/docs") || request.getRequestURI()
.contains("/favicon.ico") || request.getRequestURI().contains("/h2-console") ||
request.getRequestURI().contains("/swagger-ui") ||
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/meltingpot/server/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protected SecurityFilterChain filterChain(HttpSecurity http, TokenProvider token
//TODO 개발 μ™„λ£Œ ν›„ permitAll μ‚­μ œ
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/auth/**", "/ws/**","/api/v1/**").permitAll()
.requestMatchers("/ws/**","/api/v1/**").permitAll()
.anyRequest().authenticated()
);

Expand Down
Loading

0 comments on commit f1a3da8

Please sign in to comment.