Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
cea9ec2
add: ``Member`` 컨트롤러 클래스 선언
snowykte0426 Feb 8, 2025
ab7dd85
add: ``/member`` 엔드포인트 선언
snowykte0426 Feb 8, 2025
8750a0c
add: ``PATCH /members/password`` 엔드포인트 용 Request DTO 정의
snowykte0426 Feb 8, 2025
7ea18f3
add: 사용자 권한 정의 Enum 클래스 선언
snowykte0426 Feb 8, 2025
baad175
add: 사용자 정보 조회 API Response DTO 정의
snowykte0426 Feb 8, 2025
763f579
add: ``Member`` 도메인 객체 생성
snowykte0426 Feb 8, 2025
843458c
update: 확장 가능한 ``BaseEntity``에 접근제어자 및 Get 메서드 추가
snowykte0426 Feb 8, 2025
8506d4e
add: ``member`` JPA Entity 클래스 정의
snowykte0426 Feb 8, 2025
93ba1b9
add: Mapper 인터페이스 정의 및 ``Member`` 도메인 모델-JPA Entity 간 Mapper 구현
snowykte0426 Feb 8, 2025
148776b
docs:: ERD 임베드 추가
snowykte0426 Feb 10, 2025
26c2f1a
docs:: 테이블과 기타 요소 분리
snowykte0426 Feb 10, 2025
bcebee6
delete: ``BaseUuidEntity`` 삭제
snowykte0426 Feb 11, 2025
804e61b
update: ``BaseIdEntity`` id 필드 접근 제어자 변경
snowykte0426 Feb 11, 2025
4ce1874
update: Auto Increment 기반 PK로 변경
snowykte0426 Feb 11, 2025
9c99206
update: Entity 클래스 필드 변동에 따른 변경
snowykte0426 Feb 11, 2025
6a389c4
docs: README 확장자 변경
snowykte0426 Feb 11, 2025
a213273
add: ``@Adapter`` 어노테이션 정의
snowykte0426 Feb 11, 2025
e2dbcbe
update: Record로 변경
snowykte0426 Feb 11, 2025
a37b47a
add: Application 계층 Port/Adapter 구현
snowykte0426 Feb 11, 2025
537ac7f
add: ``toResponse`` 메서드 추가
snowykte0426 Feb 11, 2025
c480219
add: Member Persistence Port 정의
snowykte0426 Feb 11, 2025
248f9d9
add: Member Persistence Adapter 구현
snowykte0426 Feb 11, 2025
9c12989
add: Member Repository 구현
snowykte0426 Feb 11, 2025
f5da626
add: ``@UseCase`` 어노테이션 정의
snowykte0426 Feb 11, 2025
7f11125
add: 모든 Member 조회 UseCase 구현
snowykte0426 Feb 11, 2025
871f89f
add: Member 검색 UseCase 구현
snowykte0426 Feb 11, 2025
5210278
add: 예외 반환 설정 및 관련 클래스 선언
snowykte0426 Feb 11, 2025
d8a48f8
add: Member 데이터가 없을 시 발행될 예외 정의
snowykte0426 Feb 11, 2025
d47ea1d
add: 전체 사용자 조회,사용자 검색 엔드포인트 구현
snowykte0426 Feb 11, 2025
463a7a7
chore: TODO 주석 추가
snowykte0426 Feb 12, 2025
1dbd017
delete: ``Read``,``Write`` Port 구분 제거
snowykte0426 Feb 12, 2025
182b0b0
test: ``QueryAllMemberUseCase``,``SearchMemberUseCase`` Test 추가
snowykte0426 Feb 12, 2025
98d174f
test: ``contextLoads`` 테스트에서 ``test`` 프로파일 설정
snowykte0426 Feb 12, 2025
63876a5
update: 메서드 및 클래스 이름 변경
snowykte0426 Feb 13, 2025
ea542fa
update: Presentation 계층 메서드명 변경
snowykte0426 Feb 13, 2025
98d3435
Merge branch 'feature/booking-api' of https://github.com/Team-Ampersa…
snowykte0426 Mar 10, 2025
117c484
add: 사용자 ID로 Booking 탐색 메서드 추가
snowykte0426 Mar 10, 2025
25b9fbe
add: Member 객체 내부의 Booking 객체 반환용 DTO 클래스 추가
snowykte0426 Mar 10, 2025
1c8e918
update: 예약 필드 구현
snowykte0426 Mar 10, 2025
8effff2
add: 현재 인증된 사용자의 정보 조회 비즈니스 로직 클래스 구현
snowykte0426 Mar 10, 2025
d075f02
update: 현재 인증된 사용자의 정보 조회 메서드 구현
snowykte0426 Mar 10, 2025
739cfbf
add: ``UpdatePasswordUseCase`` 클래스 선언
snowykte0426 Mar 16, 2025
cf3bf20
fix: Adapter DI 문제 수정
snowykte0426 Mar 16, 2025
bd47595
delete: 프로젝트 계획 변경으로 인한 DTO 레코드 삭제
snowykte0426 Mar 16, 2025
c8f3a16
delete: 불필요 스케쥴링 서비스 클래스 제거
snowykte0426 Mar 23, 2025
4034a3f
add: ``AuthCode`` 도메인 DTO 정의
snowykte0426 Mar 24, 2025
011a74f
add: ``AuthCode`` 영속 계층 정의 및 구현
snowykte0426 Mar 24, 2025
f6e7f15
add: ``Authentication`` 관련 Domain,Persistence 계층 정의 및 구현
snowykte0426 Mar 24, 2025
e6f9571
add: 이메일 인증 횟수 초과 및 인증 객체 미존재 시 예외 추가
snowykte0426 Mar 24, 2025
4d0642f
update: 엔드포인트 설정 추가 및 불필요 예외 발행 제거
snowykte0426 Mar 24, 2025
04a2dc7
update: 이메일 인증 어댑터를 AuthCode 관련 리포지토리로 변경 및 메서드 수정
snowykte0426 Mar 24, 2025
f272f3f
fix: 실 기능을 하지 않던 이메일 인증 서비스 구현
snowykte0426 Mar 24, 2025
f6e3ac2
add: 비밀번호 변경 서비스 클래스 구현
snowykte0426 Mar 24, 2025
cbbe76c
update: ``PATCH /{memberId}/password`` 엔드포인트 정의
snowykte0426 Mar 24, 2025
0892c1d
update: 이메일 발송 설정에 연결 타임아웃 및 필수 TLS 설정 추가
snowykte0426 Mar 24, 2025
30d3dac
update: 비밀번호 변경 엔드포인트에 대한 접근 권한 설정 추가
snowykte0426 Mar 24, 2025
947687d
docs: 환경변수 관련 설정 추가
snowykte0426 Mar 24, 2025
51ff10e
update: ``AuthController`` 코드 포맷팅
snowykte0426 Mar 24, 2025
5056e2e
update: 회원가입 시 이메일 인증 추가
snowykte0426 Mar 24, 2025
480c01b
update: 조건문 간소화
snowykte0426 Mar 24, 2025
0727811
test: 현재 인증된 사용자 조회 및 비밀번호 변경 Use Case 클래스 테스트 코드 추가
snowykte0426 Mar 24, 2025
8b23b1f
test: ``FindCurrentMemberUseCase``와 ``UpdatePasswordUseCase``의 단위 테스트 구현
snowykte0426 Mar 24, 2025
074540a
fix: 올바르지 않은 인자 호출 순서 수정
snowykte0426 Mar 24, 2025
78bfd36
update: 완료된 ``TODO`` 주석 제거
snowykte0426 Mar 24, 2025
f6c6990
fix: HTTP 상태 코드를 ``RESET_CONTENT``에서 ``NO_CONTENT``로 수정
snowykte0426 Mar 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ DB_USER=string
DB_PASSWORD=string
REDIS_HOST=string
REDIS_PORT=number
REDIS_PASSWORD=string
REDIS_PASSWORD=string
EMAIL_HOST=string
EMAIL_PORT=number
EMAIL_USERNAME=string
EMAIL_PASSWORD=string
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ampersand.groom.domain.auth.application.port;

import com.ampersand.groom.domain.auth.domain.Authentication;

public interface AuthenticationPersistencePort {

Boolean existsAuthenticationByEmail(String email);

Authentication findAuthenticationByEmail(String email);

void saveAuthentication(Authentication authentication);
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package com.ampersand.groom.domain.auth.application.port;

import com.ampersand.groom.domain.auth.persistence.EmailVerification;

import java.time.LocalDateTime;
import java.util.Optional;
import com.ampersand.groom.domain.auth.domain.AuthCode;

public interface EmailVerificationPort {

// 인증 정보 저장
EmailVerification save(EmailVerification emailVerification);
// 코드로 인증 코드 존재 여부 조회
Boolean existsAuthCodeByCode(String code);

// 인증 코드로 이메일 조회
Optional<EmailVerification> findByCode(String code);
// 코드로 인증 코드 조회
AuthCode findAuthCodeByCode(String code);

// 이메일로 인증 정보 조회
Optional<EmailVerification> findByEmail(String email);
// 인증 코드 저장
void saveAuthCode(AuthCode authCode);

// 만료된 인증 정보 삭제
void deleteAllExpired(LocalDateTime now);
// 인증 코드 삭제
void deleteAuthCodeByCode(String code);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ampersand.groom.domain.auth.application.service;

import com.ampersand.groom.domain.auth.application.port.AuthPort;
import com.ampersand.groom.domain.auth.application.port.AuthenticationPersistencePort;
import com.ampersand.groom.domain.auth.domain.JwtToken;
import com.ampersand.groom.domain.auth.exception.*;
import com.ampersand.groom.domain.auth.presentation.data.request.SignupRequest;
Expand All @@ -22,6 +23,7 @@ public class AuthService {
private final JwtService jwtService;
private final PasswordEncoder passwordEncoder;
private final AuthPort authPort;
private final AuthenticationPersistencePort authenticationPersistencePort;

@Value("${spring.jwt.token.access-expiration}")
private long accessTokenExpiration;
Expand All @@ -46,6 +48,10 @@ public JwtToken refreshToken(String refreshToken) {

public void signup(SignupRequest request) {
checkUserExists(request.getEmail());
if(!authenticationPersistencePort.existsAuthenticationByEmail(request.getEmail())
|| !authenticationPersistencePort.findAuthenticationByEmail(request.getEmail()).getVerified()) {
throw new UserForbiddenException();
}
MemberJpaEntity newUser = createNewUser(request, calculateGenerationFromEmail(request.getEmail()));
authPort.save(newUser);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
package com.ampersand.groom.domain.auth.application.service;

import com.ampersand.groom.domain.auth.application.port.AuthenticationPersistencePort;
import com.ampersand.groom.domain.auth.application.port.EmailVerificationPort;
import com.ampersand.groom.domain.auth.domain.AuthCode;
import com.ampersand.groom.domain.auth.domain.Authentication;
import com.ampersand.groom.domain.auth.exception.EmailAuthRateLimitException;
import com.ampersand.groom.domain.auth.exception.EmailFormatInvalidException;
import com.ampersand.groom.domain.auth.exception.VerificationCodeFormatInvalidException;
import com.ampersand.groom.domain.auth.exception.VerificationCodeExpiredOrInvalidException;
import com.ampersand.groom.domain.auth.persistence.EmailVerification;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import java.util.Random;

@Slf4j
@Service
@RequiredArgsConstructor
public class EmailVerificationService {

private final AuthenticationPersistencePort authenticationPersistencePort;
private final EmailVerificationPort emailVerificationPort;
private final JavaMailSender javaMailSender;

private static final int MAX_EMAIL_LENGTH = 16;
private static final int CODE_LENGTH = 8;
private static final int MAX_ATTEMPT_COUNT = 5;
private static final long TTL = 300L;


//8자리 숫자 인증 코드 생성
// 인증 코드 생성
private String generateVerificationCode() {
Random random = new Random();
int code = 10000000 + random.nextInt(90000000);
int code = 10000000 + new Random().nextInt(90000000);
return String.valueOf(code);
}

// 이메일 전송 메서드
// 이메일 전송
private void sendEmail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
Expand All @@ -39,47 +45,101 @@ private void sendEmail(String to, String subject, String text) {
javaMailSender.send(message);
}

// 회원가입 인증 이메일 전송
public void sendSignupVerificationEmail(String email) {
verifyEmail(email);
// 인증 이메일 전송 (회원가입 / 비밀번호 재설정 공통 처리)
private void sendVerificationEmail(String email, String subject) {
validateEmailFormat(email);
checkAttemptCount(email);
increaseAttemptCount(email);

String code = generateVerificationCode();
sendEmail(email, "회원가입 인증", "귀하의 인증 코드는: " + code);
sendEmail(email, subject, "귀하의 인증 코드는: " + code);

EmailVerification emailVerification = new EmailVerification(email, code);

emailVerificationPort.save(emailVerification);
AuthCode authCode = AuthCode.builder()
.email(email)
.code(code)
.ttl(TTL)
.build();

emailVerificationPort.saveAuthCode(authCode);
}

// 비밀번호 변경을 위한 인증 이메일 전송
public void sendPasswordResetEmail(String email) {
verifyEmail(email);
String code = generateVerificationCode();
sendEmail(email, "비밀번호 변경 인증", "귀하의 인증 코드는: " + code);
// 회원가입용 인증 메일 전송
public void sendSignupVerificationEmail(String email) {
sendVerificationEmail(email, "회원가입 인증");
}

EmailVerification emailVerification = new EmailVerification(email, code);
emailVerificationPort.save(emailVerification);
// 비밀번호 재설정 인증 메일 전송
public void sendPasswordResetEmail(String email) {
sendVerificationEmail(email, "비밀번호 변경 인증");
}

// 인증 코드 검증
public void verifyCode(String code) {
if(code == null || code.length() != CODE_LENGTH) {
if (code == null || code.length() != CODE_LENGTH) {
throw new VerificationCodeFormatInvalidException();
}

EmailVerification emailVerification = emailVerificationPort.findByCode(code)
.orElseThrow(VerificationCodeExpiredOrInvalidException::new);

if (!emailVerificationPort.existsAuthCodeByCode(code)) {
throw new VerificationCodeExpiredOrInvalidException();
}

emailVerification.setIsVerified(true);
emailVerificationPort.save(emailVerification);
AuthCode authCode = emailVerificationPort.findAuthCodeByCode(code);
emailVerificationPort.deleteAuthCodeByCode(code);
markEmailVerified(authCode.getEmail());
}

// 이메일 검증
public void verifyEmail(String email) {
if(email == null || email.length() != MAX_EMAIL_LENGTH) {
// 이메일 형식 검증
private void validateEmailFormat(String email) {
if (email == null || email.length() != MAX_EMAIL_LENGTH) {
throw new EmailFormatInvalidException();
}
}

// 인증 시도 횟수 초과 체크
private void checkAttemptCount(String email) {
if (authenticationPersistencePort.existsAuthenticationByEmail(email)) {
int attempts = authenticationPersistencePort.findAuthenticationByEmail(email).getAttemptCount();
if (attempts >= MAX_ATTEMPT_COUNT) {
throw new EmailAuthRateLimitException();
}
}
}

// 인증 시도 횟수 증가
private void increaseAttemptCount(String email) {
Authentication existingAuth = authenticationPersistencePort.existsAuthenticationByEmail(email)
? authenticationPersistencePort.findAuthenticationByEmail(email)
: null;
Authentication updatedAuth;
if(existingAuth != null) {
updatedAuth = Authentication.builder()
.email(email)
.attemptCount(existingAuth.getAttemptCount() + 1)
.verified(existingAuth.getVerified())
.ttl(existingAuth.getTtl())
.build();
} else {
updatedAuth = Authentication.builder()
.email(email)
.attemptCount(1)
.verified(false)
.ttl(TTL)
.build();
}
authenticationPersistencePort.saveAuthentication(updatedAuth);
}

// 인증 완료 처리
private void markEmailVerified(String email) {
Authentication authentication = authenticationPersistencePort.findAuthenticationByEmail(email);

Authentication updatedAuth = Authentication.builder()
.email(email)
.attemptCount(authentication.getAttemptCount())
.verified(true)
.ttl(authentication.getTtl())
.build();

authenticationPersistencePort.saveAuthentication(updatedAuth);
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/ampersand/groom/domain/auth/domain/AuthCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ampersand.groom.domain.auth.domain;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class AuthCode {
private final String email;
private final String code;
private final Long ttl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ampersand.groom.domain.auth.domain;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class Authentication {
private String email;
private int attemptCount;
private Boolean verified;
private Long ttl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.exception;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class AuthenticationNotFoundException extends GroomException {
public AuthenticationNotFoundException() {
super(ErrorCode.AUTHENTICATION_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ampersand.groom.domain.auth.exception;

import com.ampersand.groom.global.error.ErrorCode;
import com.ampersand.groom.global.error.exception.GroomException;

public class EmailAuthRateLimitException extends GroomException {
public EmailAuthRateLimitException() {
super(ErrorCode.EMAIL_AUTHENTICATION_TOO_MANY_REQUESTS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.ampersand.groom.domain.auth.persistence.adapter.email;

import com.ampersand.groom.domain.auth.application.port.AuthenticationPersistencePort;
import com.ampersand.groom.domain.auth.domain.Authentication;
import com.ampersand.groom.domain.auth.exception.AuthenticationNotFoundException;
import com.ampersand.groom.domain.auth.persistence.mapper.AuthenticationMapper;
import com.ampersand.groom.domain.auth.persistence.repository.AuthenticationRedisRepository;
import com.ampersand.groom.global.annotation.adapter.Adapter;
import com.ampersand.groom.global.annotation.adapter.constant.AdapterType;
import lombok.RequiredArgsConstructor;

@Adapter(AdapterType.OUTBOUND)
@RequiredArgsConstructor
public class AuthenticationPersistenceAdapter implements AuthenticationPersistencePort {

private final AuthenticationRedisRepository authenticationRedisRepository;
private final AuthenticationMapper authenticationMapper;

@Override
public Boolean existsAuthenticationByEmail(String email) {
return authenticationRedisRepository.existsById(email);
}

@Override
public Authentication findAuthenticationByEmail(String email) {
return authenticationMapper.toDomain(authenticationRedisRepository.findById(email).orElseThrow(AuthenticationNotFoundException::new));
}

@Override
public void saveAuthentication(Authentication authentication) {
authenticationRedisRepository.save(authenticationMapper.toEntity(authentication));
}
}
Loading