Skip to content

Commit

Permalink
Merge pull request #98 from JangYouJung/develop
Browse files Browse the repository at this point in the history
[ Feature ] ์ด๋ฉ”์ผ ์ธ์ฆ ๊ตฌํ˜„ ๋ฐ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€/์‚ญ์ œ ๊ตฌํ˜„
  • Loading branch information
JangYouJung authored Jun 10, 2024
2 parents 6976b55 + b94d711 commit 7fe3730
Show file tree
Hide file tree
Showing 21 changed files with 457 additions and 30 deletions.
1 change: 0 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ jobs:
port: 22
uploads: |
./build/libs/ => /root/apache-tomcat-10.1.24/build-target/
- name: Change old WAR to new WAR
uses: appleboy/[email protected]
with:
Expand Down
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,10 +41,15 @@ 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 ์ƒ์„ฑ
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
@@ -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/**", "/chat/**").permitAll()
.requestMatchers("/auth/**", "/api/v1/**").permitAll()
.requestMatchers("/ws/**","/api/v1/**").permitAll()
.anyRequest().authenticated()
);

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/meltingpot/server/domain/entity/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package meltingpot.server.domain.entity;

public final class Constants {

// ์ด๋ฉ”์ผ ์ธ์ฆ ์‹œ๋„ ์ œํ•œ ํšŸ์ˆ˜
public static final Integer LIMIT_ATTEMPT_COUNT = 5;

// ์ด๋ฉ”์ผ ์ธ์ฆ ์ œํ•œ ์ฟจํƒ€์ž„ ์‹œ๊ฐ„(๋ถ„)
public static final Integer COOL_TIME_MINUTE = 5;

// ์ด๋ฉ”์ผ ์ธ์ฆ ๋ฒˆํ˜ธ ํ™•์ธ ์ œํ•œ ์‹œ๊ฐ„(๋ถ„)
public static final Integer AUTH_TIME_LIMIT = 10;

}
Loading

0 comments on commit 7fe3730

Please sign in to comment.