Skip to content

Commit

Permalink
Merge branch 'dev' into fix/PW-156-cicd
Browse files Browse the repository at this point in the history
  • Loading branch information
jinlee1703 authored Mar 26, 2024
2 parents 667fadf + 1b41db4 commit 655ecdb
Show file tree
Hide file tree
Showing 46 changed files with 801 additions and 90 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ jobs:
./gradlew build --stacktrace --info -x test
shell: bash

# 3. Docker 이미지 build 및 push
# 4. Docker 이미지 build 및 push
- name: docker build and push
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t jinlee1703/pennyway-was .
docker push jinlee1703/pennyway-was
# 4. AWS SSM을 통한 Run-Command (Docker 이미지 pull 후 docker-compose를 통한 실행)
# 5. AWS SSM을 통한 Run-Command (Docker 이미지 pull 후 docker-compose를 통한 실행)
- name: AWS SSM Send-Command
uses: peterkimzz/aws-ssm-send-command@master
id: ssm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.co.pennyway.api.apis.auth.dto.PhoneVerificationDto;
import kr.co.pennyway.api.apis.auth.dto.SignUpReq;
import kr.co.pennyway.api.apis.auth.usecase.AuthUseCase;
import kr.co.pennyway.api.common.response.SuccessResponse;
Expand All @@ -26,11 +27,25 @@
@Tag(name = "[인증 API]")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
@RequestMapping("/v1/auth")
public class AuthController {
private final AuthUseCase authUseCase;
private final CookieUtil cookieUtil;

@Operation(summary = "인증번호 전송")
@PostMapping("/phone")
// TODO: Spring Security 설정 후 @PreAuthorize("permitAll()") 추가 && ip 당 횟수 제한
public ResponseEntity<?> sendCode(@RequestBody @Validated PhoneVerificationDto.PushCodeReq request) {
return ResponseEntity.ok(SuccessResponse.from("sms", authUseCase.sendCode(request)));
}

@Operation(summary = "인증번호 검증")
@PostMapping("/phone/verification")
// TODO: Spring Security 설정 후 @PreAuthorize("permitAll()") 추가
public ResponseEntity<?> verifyCode(@RequestBody @Validated PhoneVerificationDto.VerifyCodeReq request) {
return ResponseEntity.ok(SuccessResponse.from("sms", authUseCase.verifyCode(request)));
}

@Operation(summary = "일반 회원가입")
@PostMapping("/sign-up")
// TODO: Spring Security 설정 후 @PreAuthorize("isAnonymous()") 추가
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package kr.co.pennyway.api.apis.auth.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

import java.time.LocalDateTime;

public class PhoneVerificationDto {
@Schema(title = "인증번호 요청 DTO", description = "전화번호로 인증번호 송신 요청을 위한 DTO")
public record PushCodeReq(
@Schema(description = "전화번호", example = "01012345678")
@NotBlank(message = "전화번호는 필수입니다.")
@Pattern(regexp = "^01[01]-\\d{4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다.")
String phone
) {
}

@Schema(title = "인증번호 발송 응답 DTO", description = "전화번호로 인증번호 송신 응답을 위한 DTO")
public record PushCodeRes(
@Schema(description = "수신자 번호")
String to,
@Schema(description = "발송 시간")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime sendAt,
@Schema(description = "만료 시간 (default: 3분)", example = "2021-08-01T00:00:00")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime expiresAt
) {
/**
* 인증번호 발송 응답 객체 생성
*
* @param to String : 수신자 번호
* @param sendAt LocalDateTime : 발송 시간
* @param expiresAt LocalDateTime : 만료 시간 (default: 5분)
*/
public static PushCodeRes of(String to, LocalDateTime sendAt, LocalDateTime expiresAt) {
return new PushCodeRes(to, sendAt, expiresAt);
}
}

@Schema(title = "인증번호 검증 DTO", description = "전화번호로 인증번호 검증 요청을 위한 DTO")
public record VerifyCodeReq(
@Schema(description = "전화번호", example = "01012345678")
@NotBlank(message = "전화번호는 필수입니다.")
@Pattern(regexp = "^01[01]-\\d{4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다.")
String phone,
@Schema(description = "6자리 정수 인증번호", example = "123456")
@NotBlank(message = "인증번호는 필수입니다.")
@Pattern(regexp = "^\\d{6}$", message = "인증번호는 6자리 숫자여야 합니다.")
String code
) {
}

@Schema(title = "인증번호 검증 응답 DTO")
public record VerifyCodeRes(
@Schema(description = "코드 일치 여부 : 일치하지 않으면 예외이므로 성공하면 언제나 true", example = "true")
Boolean code,
@Schema(description = "oauth 사용자 여부", example = "true")
Boolean oauth,
@Schema(description = "기존 사용자 아이디", example = "pennyway")
@JsonInclude(JsonInclude.Include.NON_NULL)
String username
) {
public static VerifyCodeRes valueOf(Boolean isValidCode, Boolean isOauthUser, String username) {
return new VerifyCodeRes(isValidCode, isOauthUser, username);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kr.co.pennyway.api.apis.auth.helper;

import kr.co.pennyway.common.annotation.Helper;
import kr.co.pennyway.common.exception.GlobalErrorException;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.exception.UserErrorCode;
import kr.co.pennyway.domain.domains.user.exception.UserErrorException;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;

@Slf4j
@Helper
@RequiredArgsConstructor
public class UserSyncHelper {
private final UserService userService;

/**
* 일반 회원가입 시 이미 가입된 회원인지 확인
*
* @param phone String : 전화번호
* @return Pair<Boolean, String> : 이미 가입된 회원인지 여부 (TRUE: 가입되지 않은 회원, FALSE: 가입된 회원), 가입된 회원인 경우 회원 ID 반환
* @throws UserErrorException : 이미 일반 회원가입을 한 유저인 경우
*/
public Pair<Boolean, String> isSignedUserWhenGeneral(String phone) {
User user;
try {
user = userService.readUserByPhone(phone);
} catch (GlobalErrorException e) {
log.info("User not found. phone: {}", phone);
return Pair.of(Boolean.TRUE, null);
}

if (user.getPassword() != null) {
log.warn("User already exists. phone: {}", phone);
throw new UserErrorException(UserErrorCode.ALREADY_SIGNUP);
}

return Pair.of(Boolean.FALSE, user.getUsername());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package kr.co.pennyway.api.apis.auth.mapper;

import kr.co.infra.common.jwt.JwtProvider;
import kr.co.pennyway.api.common.annotation.AccessTokenStrategy;
import kr.co.pennyway.api.common.annotation.RefreshTokenStrategy;
import kr.co.pennyway.api.common.security.jwt.Jwts;
Expand All @@ -10,6 +9,7 @@
import kr.co.pennyway.domain.common.redis.refresh.RefreshToken;
import kr.co.pennyway.domain.common.redis.refresh.RefreshTokenService;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.infra.common.jwt.JwtProvider;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package kr.co.pennyway.api.apis.auth.mapper;

import kr.co.pennyway.api.apis.auth.dto.PhoneVerificationDto;
import kr.co.pennyway.api.common.exception.PhoneVerificationErrorCode;
import kr.co.pennyway.api.common.exception.PhoneVerificationException;
import kr.co.pennyway.common.annotation.Mapper;
import kr.co.pennyway.domain.common.redis.phone.PhoneVerificationService;
import kr.co.pennyway.domain.common.redis.phone.PhoneVerificationType;
import kr.co.pennyway.infra.client.aws.sms.SmsDto;
import kr.co.pennyway.infra.client.aws.sms.SmsProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;

@Slf4j
@Mapper
@RequiredArgsConstructor
public class PhoneVerificationMapper {
private final PhoneVerificationService phoneVerificationService;
private final SmsProvider smsProvider;

/**
* 휴대폰 번호로 인증 코드를 발송하고 캐싱한다. (5분간 유효)
*
* @param request {@link PhoneVerificationDto.PushCodeReq}
* @param codeType {@link PhoneVerificationType}
* @return {@link PhoneVerificationDto.PushCodeRes}
*/
public PhoneVerificationDto.PushCodeRes sendCode(PhoneVerificationDto.PushCodeReq request, PhoneVerificationType codeType) {
SmsDto.Info info = smsProvider.sendCode(SmsDto.To.of(request.phone()));
LocalDateTime expiresAt = phoneVerificationService.create(request.phone(), info.code(), codeType);
return PhoneVerificationDto.PushCodeRes.of(request.phone(), info.requestAt(), expiresAt);
}

/**
* 휴대폰 번호로 인증 코드를 확인한다.
*
* @param request {@link PhoneVerificationDto.VerifyCodeReq}
* @param codeType {@link PhoneVerificationType}
* @return Boolean : 인증 코드가 유효한지 여부 (TRUE: 유효, 실패하는 경우 예외가 발생하므로 FALSE가 반환되지 않음)
* @throws PhoneVerificationException : 전화번호가 만료되었거나 유효하지 않은 경우(EXPIRED_OR_INVALID_PHONE), 인증 코드가 유효하지 않은 경우(IS_NOT_VALID_CODE)
*/
public Boolean isValidCode(PhoneVerificationDto.VerifyCodeReq request, PhoneVerificationType codeType) {
String expectedCode;
try {
expectedCode = phoneVerificationService.readByPhone(request.phone(), codeType);
} catch (IllegalArgumentException e) {
throw new PhoneVerificationException(PhoneVerificationErrorCode.EXPIRED_OR_INVALID_PHONE);
}

if (!expectedCode.equals(request.code()))
throw new PhoneVerificationException(PhoneVerificationErrorCode.IS_NOT_VALID_CODE);
return Boolean.TRUE;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package kr.co.pennyway.api.apis.auth.usecase;

import kr.co.pennyway.api.apis.auth.dto.PhoneVerificationDto;
import kr.co.pennyway.api.apis.auth.dto.SignUpReq;
import kr.co.pennyway.api.apis.auth.helper.UserSyncHelper;
import kr.co.pennyway.api.apis.auth.mapper.JwtAuthMapper;
import kr.co.pennyway.api.apis.auth.mapper.PhoneVerificationMapper;
import kr.co.pennyway.api.common.security.jwt.Jwts;
import kr.co.pennyway.common.annotation.UseCase;
import kr.co.pennyway.domain.common.redis.phone.PhoneVerificationService;
import kr.co.pennyway.domain.common.redis.phone.PhoneVerificationType;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.exception.UserErrorException;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -16,8 +22,24 @@
@RequiredArgsConstructor
public class AuthUseCase {
private final UserService userService;
private final UserSyncHelper userSyncHelper;

private final JwtAuthMapper jwtAuthMapper;
private final PhoneVerificationMapper phoneVerificationMapper;
private final PhoneVerificationService phoneVerificationService;

public PhoneVerificationDto.PushCodeRes sendCode(PhoneVerificationDto.PushCodeReq request) {
return phoneVerificationMapper.sendCode(request, PhoneVerificationType.SIGN_UP);
}

public PhoneVerificationDto.VerifyCodeRes verifyCode(PhoneVerificationDto.VerifyCodeReq request) {
Boolean isValidCode = phoneVerificationMapper.isValidCode(request, PhoneVerificationType.SIGN_UP);
Pair<Boolean, String> isOauthUser = checkOauthUser(request.phone());

phoneVerificationService.extendTimeToLeave(request.phone(), PhoneVerificationType.SIGN_UP);

return PhoneVerificationDto.VerifyCodeRes.valueOf(isValidCode, isOauthUser.getKey(), isOauthUser.getValue());
}

@Transactional
public Pair<Long, Jwts> signUp(SignUpReq.General request) {
Expand All @@ -28,4 +50,13 @@ public Pair<Long, Jwts> signUp(SignUpReq.General request) {

return Pair.of(user.getId(), jwtAuthMapper.createToken(user));
}

private Pair<Boolean, String> checkOauthUser(String phone) {
try {
return userSyncHelper.isSignedUserWhenGeneral(phone);
} catch (UserErrorException e) {
phoneVerificationService.delete(phone, PhoneVerificationType.SIGN_UP);
throw e;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package kr.co.pennyway.api.common.exception;

import kr.co.pennyway.common.exception.BaseErrorCode;
import kr.co.pennyway.common.exception.CausedBy;
import kr.co.pennyway.common.exception.ReasonCode;
import kr.co.pennyway.common.exception.StatusCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum PhoneVerificationErrorCode implements BaseErrorCode {
// 401 Unauthorized
EXPIRED_OR_INVALID_PHONE(StatusCode.UNAUTHORIZED, ReasonCode.MISSING_OR_INVALID_AUTHENTICATION_CREDENTIALS, "만료되었거나 등록되지 않은 휴대폰 정보입니다."),
IS_NOT_VALID_CODE(StatusCode.UNAUTHORIZED, ReasonCode.MISSING_OR_INVALID_AUTHENTICATION_CREDENTIALS, "인증코드가 일치하지 않습니다."),
;

private final StatusCode statusCode;
private final ReasonCode reasonCode;
private final String message;

@Override
public CausedBy causedBy() {
return CausedBy.of(statusCode, reasonCode);
}

@Override
public String getExplainError() throws NoSuchFieldError {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.pennyway.api.common.exception;

import kr.co.pennyway.common.exception.CausedBy;
import kr.co.pennyway.common.exception.GlobalErrorException;

public class PhoneVerificationException extends GlobalErrorException {
private final PhoneVerificationErrorCode errorCode;

public PhoneVerificationException(PhoneVerificationErrorCode errorCode) {
super(errorCode);
this.errorCode = errorCode;
}

@Override
public CausedBy causedBy() {
return errorCode.causedBy();
}

public String getExplainError() {
return errorCode.getExplainError();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Schema(description = "API 응답 - 성공")
public class SuccessResponse<T> {
@Schema(description = "응답 코드", defaultValue = "2000000")
private final String code = "2000000";
@Schema(description = "응답 코드", defaultValue = "2000")
private final String code = "2000";
@Schema(description = "응답 메시지", example = """
data: {
"aDomain": { // 단수명사는 object 형태로 반환
Expand Down
Loading

0 comments on commit 655ecdb

Please sign in to comment.