Skip to content

Commit

Permalink
[BE] feat: 학생 인증 메일 전송 잦은 재요청시 예외처리 (#466) (#467)
Browse files Browse the repository at this point in the history
* feat: StudentCode unique 제약조건 추가

* refactor: 불필요한 setUp 메서드 제거

* feat: StudnetCode 발행 일자 필드 추가 및 재발행 가능 여부 확인 기능 구현

* feat: 인증번호 재전송 가능 확인 기능 구현

* fix: duplicated key exception으로 insert가 안되는 버그 수정

* refactor: 기존 테스트와 새로운 테스트 병합

* feat: SetUpMockito 클래스 생성 및 적용

* refactor: studentCode unique 제약조건 수정

* refactor: StudentCode에서 BaseTimeEntity 상속받지 않도록 수정

* refactor: username 파라미터로 전달

* refactor: studentCode issued_at not null 제약조건 추가

* refactor: 학생 인증 코드 재발급 로직 update를 활용하도록 수정

* refactor: 429 Too Many Request 적용

* refactor: flyway version 수정

* refactor: StudentCode BaseTimeEntity 상속받지 않도록 수정

* refactor: student_code UNIQUE 제약조건 추가 flyway script 수정
  • Loading branch information
xxeol2 authored and BGuga committed Oct 17, 2023
1 parent 5a4b092 commit 943d4e6
Show file tree
Hide file tree
Showing 12 changed files with 371 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum ErrorCode {
TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."),
INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."),


// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
INVALID_AUTH_TOKEN("올바르지 않은 로그인 토큰입니다."),
Expand All @@ -46,6 +47,9 @@ public enum ErrorCode {
TICKET_NOT_FOUND("존재하지 않는 티켓입니다."),
SCHOOL_NOT_FOUND("존재하지 않는 학교입니다."),

// 429
TOO_FREQUENT_REQUESTS("너무 잦은 요청입니다. 잠시 후 다시 시도해주세요."),

// 500
INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."),
INVALID_ENTRY_CODE_PERIOD("올바르지 않은 입장코드 유효기간입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.festago.common.exception;

public class TooManyRequestException extends FestaGoException {

public TooManyRequestException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.festago.common.exception.ForbiddenException;
import com.festago.common.exception.InternalServerException;
import com.festago.common.exception.NotFoundException;
import com.festago.common.exception.TooManyRequestException;
import com.festago.common.exception.UnauthorizedException;
import com.festago.common.exception.dto.ErrorResponse;
import com.festago.presentation.auth.AuthenticateContext;
Expand Down Expand Up @@ -86,6 +87,12 @@ public ResponseEntity<ErrorResponse> handle(NotFoundException e, HttpServletRequ
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e));
}

@ExceptionHandler(TooManyRequestException.class)
public ResponseEntity<ErrorResponse> handle(TooManyRequestException e, HttpServletRequest request) {
logInfo(e, request);
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(ErrorResponse.from(e));
}

@ExceptionHandler(InternalServerException.class)
public ResponseEntity<ErrorResponse> handle(InternalServerException e, HttpServletRequest request) {
logWarn(e, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.NotFoundException;
import com.festago.common.exception.TooManyRequestException;
import com.festago.member.domain.Member;
import com.festago.member.repository.MemberRepository;
import com.festago.school.domain.School;
Expand All @@ -15,6 +16,8 @@
import com.festago.student.dto.StudentVerificateRequest;
import com.festago.student.repository.StudentCodeRepository;
import com.festago.student.repository.StudentRepository;
import java.time.Clock;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -30,18 +33,42 @@ public class StudentService {
private final SchoolRepository schoolRepository;
private final MemberRepository memberRepository;
private final StudentRepository studentRepository;
private final Clock clock;

public void sendVerificationMail(Long memberId, StudentSendMailRequest request) {
validateStudent(memberId);
validateDuplicateEmail(request);
Member member = findMember(memberId);
School school = findSchool(request.schoolId());
validate(memberId, request);
VerificationCode code = codeProvider.provide();
studentCodeRepository.deleteByMember(member);
studentCodeRepository.save(new StudentCode(code, school, member, request.username()));
saveStudentCode(code, member, school, request.username());
mailClient.send(new VerificationMailPayload(code, request.username(), school.getDomain()));
}

private Member findMember(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND));
}

private School findSchool(Long schoolId) {
return schoolRepository.findById(schoolId)
.orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND));
}

private void validate(Long memberId, StudentSendMailRequest request) {
validateFrequentRequest(memberId);
validateStudent(memberId);
validateDuplicateEmail(request);
}

private void validateFrequentRequest(Long memberId) {
studentCodeRepository.findByMemberId(memberId)
.ifPresent(code -> {
if (!code.canReissue(LocalDateTime.now(clock))) {
throw new TooManyRequestException(ErrorCode.TOO_FREQUENT_REQUESTS);
}
});
}

private void validateStudent(Long memberId) {
if (studentRepository.existsByMemberId(memberId)) {
throw new BadRequestException(ErrorCode.ALREADY_STUDENT_VERIFIED);
Expand All @@ -54,14 +81,12 @@ private void validateDuplicateEmail(StudentSendMailRequest request) {
}
}

private Member findMember(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND));
}

private School findSchool(Long schoolId) {
return schoolRepository.findById(schoolId)
.orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND));
private void saveStudentCode(VerificationCode code, Member member, School school, String username) {
studentCodeRepository.findByMemberId(member.getId())
.ifPresentOrElse(
studentCode -> studentCode.reissue(code, school, username),
() -> studentCodeRepository.save(new StudentCode(code, school, member, username))
);
}

public void verificate(Long memberId, StudentVerificateRequest request) {
Expand Down
39 changes: 35 additions & 4 deletions backend/src/main/java/com/festago/student/domain/StudentCode.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
package com.festago.student.domain;

import com.festago.common.domain.BaseTimeEntity;
import static java.time.temporal.ChronoUnit.SECONDS;

import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.InternalServerException;
import com.festago.member.domain.Member;
import com.festago.school.domain.School;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.util.StringUtils;

@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class StudentCode extends BaseTimeEntity {
public class StudentCode {

private static final int MIN_REQUEST_TERM_SECONDS = 30;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -32,21 +42,28 @@ public class StudentCode extends BaseTimeEntity {
private School school;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(unique = true)
private Member member;

private String username;

@NotNull
@LastModifiedDate
private LocalDateTime issuedAt;

public StudentCode(VerificationCode code, School school, Member member, String username) {
this(null, code, school, member, username);
this(null, code, school, member, username, null);
}

public StudentCode(Long id, VerificationCode code, School school, Member member, String username) {
public StudentCode(Long id, VerificationCode code, School school, Member member, String username,
LocalDateTime issuedAt) {
validate(username);
this.id = id;
this.code = code;
this.school = school;
this.member = member;
this.username = username;
this.issuedAt = issuedAt;
}

private void validate(String username) {
Expand All @@ -59,6 +76,16 @@ private void validate(String username) {
}
}

public boolean canReissue(LocalDateTime currentTime) {
return SECONDS.between(issuedAt, currentTime) > MIN_REQUEST_TERM_SECONDS;
}

public void reissue(VerificationCode code, School school, String username) {
this.code = code;
this.school = school;
this.username = username;
}

public Long getId() {
return id;
}
Expand All @@ -78,4 +105,8 @@ public Member getMember() {
public String getUsername() {
return username;
}

public LocalDateTime getIssuedAt() {
return issuedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface StudentCodeRepository extends JpaRepository<StudentCode, Long>
void deleteByMember(Member member);

Optional<StudentCode> findByCodeAndMember(VerificationCode code, Member member);

Optional<StudentCode> findByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- issued_at 칼럼 추가 (NOT NULL)
alter table student_code
add column issued_at datetime(6) not null
default '1999-12-31 00:00:00';

alter table student_code
alter column issued_at drop default;

-- 기존 created_at updated_at 삭제
alter table student_code
drop column created_at;

alter table student_code
drop column updated_at;

-- StudentCode의 member_id UNIQUE 제약조건 추가
alter table student_code
modify column member_id bigint unique;
Loading

0 comments on commit 943d4e6

Please sign in to comment.