Skip to content

Commit

Permalink
feat: 회원 탈퇴 기능 개선 (#229)
Browse files Browse the repository at this point in the history
* feat: 탈퇴한 회원, 관리자의 로그인 차단

* refactor: 로그인 과정 Role 확인을 Enum으로 변경

* feat: 이메일 인증 후 회원 복구하도록 api 재작성

* fix: EmailVerifyCode 유효시간 수정 (3분->5분)

* fix: secret key 제거

* fix: secret key 재수정

---------

Co-authored-by: 조은비 <[email protected]>
  • Loading branch information
kimday0326 and eunbc committed Jan 12, 2024
1 parent 2b79e13 commit ede4506
Show file tree
Hide file tree
Showing 21 changed files with 286 additions and 28 deletions.
3 changes: 2 additions & 1 deletion api/api-member/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

implementation 'org.springframework.boot:spring-boot-starter-mail:3.2.0'

// security
implementation 'org.springframework.boot:spring-boot-starter-security'
}
22 changes: 17 additions & 5 deletions api/api-member/http/test.http
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,21 @@ Content-Type: application/json

### 멤버 본인 탈퇴 (토큰 필요)
DELETE http://localhost:8081/api/v1/members/me
Authorization: Bearer {{token}}
Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpZCI6Mywic3ViIjoiZGh4bDUwQG5hdmVyLmNvbSIsImlhdCI6MTcwNDk3MzE5OCwiZXhwIjoxNzA0OTc0OTk4LCJhdXRob3JpdHkiOiJST0xFX1VTRVIifQ.6QrGoK0YJ2P9idqh5gVYZVRbZX_pNdCLLdAI9W0-npQXy_JBNAMTP2QGfSK7F9CZ

### 멤버 계정 복구 (토큰 필요)
POST http://localhost:8081/api/v1/members/restore
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer {{token}}
### 멤버 계정 복구 이메일 전송
POST http://localhost:8081/api/v1/members/send-restore-email
Content-Type: application/json

{
"email": "[email protected]"
}

### 멤버 계정 복구 (이메일 코드 포함)
PATCH http://localhost:8081/api/v1/members/confirm-restore
Content-Type: application/json

{
"email": "[email protected]",
"code": "925340"
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.pgms.apimember.dto.request.RefreshTokenRequest;
import com.pgms.apimember.dto.response.AuthResponse;
import com.pgms.apimember.service.AuthService;
import com.pgms.coredomain.domain.member.enums.Role;
import com.pgms.coredomain.response.ApiResponse;

import jakarta.validation.Valid;
Expand All @@ -27,14 +28,14 @@ public class AuthController {
*/
@PostMapping("/admin/login")
public ResponseEntity<ApiResponse<AuthResponse>> adminLogin(@Valid @RequestBody LoginRequest request) {
AuthResponse response = authService.login(request, "admin");
AuthResponse response = authService.login(request, Role.ROLE_ADMIN);
return ResponseEntity.ok(ApiResponse.ok(response));
}

@PostMapping("/members/login")
public ResponseEntity<ApiResponse<AuthResponse>> memberLogin(@Valid @RequestBody LoginRequest request) {
// TODO: 나중에 enum으로..?
AuthResponse response = authService.login(request, "member");
AuthResponse response = authService.login(request, Role.ROLE_USER);
return ResponseEntity.ok(ApiResponse.ok(response));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.pgms.apimember.dto.request.ConfirmRestoreRequest;
import com.pgms.apimember.dto.request.MemberInfoUpdateRequest;
import com.pgms.apimember.dto.request.MemberPasswordUpdateRequest;
import com.pgms.apimember.dto.request.MemberPasswordVerifyRequest;
import com.pgms.apimember.dto.request.MemberRestoreRequest;
import com.pgms.apimember.dto.request.MemberSignUpRequest;
import com.pgms.apimember.dto.response.MemberDetailGetResponse;
import com.pgms.apimember.service.MemberService;
Expand Down Expand Up @@ -67,8 +69,15 @@ public ResponseEntity<ApiResponse<Void>> deleteMyAccount(@CurrentAccount Long me
return ResponseEntity.noContent().build();
}

@PostMapping("/restore")
public ResponseEntity<ApiResponse<Long>> restoreMember(@CurrentAccount Long memberId) {
return ResponseEntity.ok(ApiResponse.ok(memberService.restoreMember(memberId)));
@PostMapping("send-restore-email")
public ResponseEntity<ApiResponse<String>> sendRestoreEmail(@RequestBody @Valid MemberRestoreRequest requestDto) {
memberService.sendRestoreEmail(requestDto);
return ResponseEntity.noContent().build();
}

@PatchMapping("/confirm-restore")
public ResponseEntity<ApiResponse<Void>> confirmRestore(@RequestBody @Valid ConfirmRestoreRequest requestDto) {
memberService.confirmRestoreMember(requestDto);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pgms.apimember.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ConfirmRestoreRequest(
@NotBlank(message = "이메일은 필수 항목입니다.")
@Email(message = "이메일 형식에 맞지 않습니다.")
String email,

@NotBlank(message = "인증 코드는 필수 항목입니다.")
@Size(min = 6, max = 6, message = "인증 코드는 6자리 입니다.")
String code
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.pgms.apimember.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record MemberRestoreRequest(
@NotBlank(message = "이메일은 필수 항목입니다.")
@Email(message = "이메일 형식에 맞지 않습니다.")
String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.pgms.apimember.email;

import java.util.Properties;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

@Configuration
public class EmailConfig {

@Value("${spring.mail.host}")
private String host;

@Value("${spring.mail.port}")
private int port;

@Value("${spring.mail.username}")
private String username;

@Value("${spring.mail.password}")
private String password;

@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;

@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttlsEnable;

@Value("${spring.mail.properties.mail.smtp.starttls.required}")
private boolean starttlsRequired;

@Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
private int connectionTimeout;

@Value("${spring.mail.properties.mail.smtp.timeout}")
private int timeout;

@Value("${spring.mail.properties.mail.smtp.writetimeout}")
private int writeTimeout;

@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());

return mailSender;
}

private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", auth);
properties.put("mail.smtp.starttls.enable", starttlsEnable);
properties.put("mail.smtp.starttls.required", starttlsRequired);
properties.put("mail.smtp.connectiontimeout", connectionTimeout);
properties.put("mail.smtp.timeout", timeout);
properties.put("mail.smtp.writetimeout", writeTimeout);

return properties;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.pgms.apimember.email;

import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

import com.pgms.apimember.exception.MemberException;
import com.pgms.coredomain.domain.common.MemberErrorCode;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class MailService {

private final JavaMailSender emailSender;

public void sendEmail(
String toEmail,
String title,
String text) {
SimpleMailMessage emailForm = createEmailForm(toEmail, title, text);
try {
emailSender.send(emailForm);
} catch (RuntimeException e) {
throw new MemberException(MemberErrorCode.EMAIL_SEND_FAIL);
}
}

private SimpleMailMessage createEmailForm(
String toEmail,
String title,
String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(toEmail);
message.setSubject(title);
message.setText(text);

return message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.pgms.apimember.dto.response.AuthResponse;
import com.pgms.apimember.exception.CustomException;
import com.pgms.coredomain.domain.common.SecurityErrorCode;
import com.pgms.coredomain.domain.member.enums.Role;
import com.pgms.coredomain.domain.member.redis.RefreshToken;
import com.pgms.coredomain.domain.member.redis.RefreshTokenRepository;
import com.pgms.coresecurity.security.jwt.JwtTokenProvider;
Expand All @@ -32,7 +33,7 @@ public class AuthService {
private final AdminUserDetailsService adminUserDetailsService;
private final MemberUserDetailsService memberUserDetailsService;

public AuthResponse login(LoginRequest request, String accountType) {
public AuthResponse login(LoginRequest request, Role accountType) {
// 인증 전의 auth 객체
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
request.email(),
Expand All @@ -49,7 +50,7 @@ public AuthResponse login(LoginRequest request, String accountType) {
String refreshToken = jwtTokenProvider.generateRefreshToken();

// redis에 토큰 정보 저장
refreshTokenRepository.save(new RefreshToken(refreshToken, accessToken, accountType,
refreshTokenRepository.save(new RefreshToken(refreshToken, accessToken, accountType.toString(),
((UserDetailsImpl)authenticated.getPrincipal()).getEmail()));
return new AuthResponse(accessToken, refreshToken);
}
Expand All @@ -74,7 +75,7 @@ public AuthResponse refresh(RefreshTokenRequest request) {
}

private UserDetailsImpl loadUserDetails(String accountType, String email) {
if ("admin".equals(accountType)) {
if (accountType.equals(Role.ROLE_ADMIN.toString())) {
return (UserDetailsImpl)adminUserDetailsService.loadUserByUsername(email);
} else {
return (UserDetailsImpl)memberUserDetailsService.loadUserByUsername(email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,44 @@

import static com.pgms.coredomain.domain.common.MemberErrorCode.*;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.pgms.apimember.dto.request.ConfirmRestoreRequest;
import com.pgms.apimember.dto.request.MemberInfoUpdateRequest;
import com.pgms.apimember.dto.request.MemberPasswordUpdateRequest;
import com.pgms.apimember.dto.request.MemberRestoreRequest;
import com.pgms.apimember.dto.request.MemberSignUpRequest;
import com.pgms.apimember.dto.response.MemberDetailGetResponse;
import com.pgms.apimember.email.MailService;
import com.pgms.apimember.exception.MemberException;
import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coredomain.domain.member.Member;
import com.pgms.coredomain.domain.member.redis.BlockedToken;
import com.pgms.coredomain.domain.member.redis.BlockedTokenRepository;
import com.pgms.coredomain.domain.member.redis.EmailVerifyCode;
import com.pgms.coredomain.domain.member.redis.EmailVerifyCodeRepository;
import com.pgms.coredomain.domain.member.repository.MemberRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

private final MailService mailService;
private final MemberRepository memberRepository;
private final BlockedTokenRepository blockedTokenRepository;
private final EmailVerifyCodeRepository emailVerifyCodeRepository;
private final PasswordEncoder passwordEncoder;

public Long signUp(MemberSignUpRequest requestDto) {
Expand Down Expand Up @@ -80,11 +93,29 @@ public void deleteMember(Long memberId) {
);
}

public Long restoreMember(Long memberId) {
Member member = memberRepository.findByIdAndIsDeletedTrue(memberId)
public void sendRestoreEmail(MemberRestoreRequest requestDto) {
memberRepository.findByEmailAndIsDeletedTrue(requestDto.email())
.orElseThrow(() -> new MemberException(MEMBER_NOT_FOUND));
String title = "[BingterPark] 계정 복구 인증 메일";
String authCode = this.createCode();
mailService.sendEmail(requestDto.email(), title, authCode);
emailVerifyCodeRepository.save(
new EmailVerifyCode(authCode, requestDto.email())
);
}

public void confirmRestoreMember(ConfirmRestoreRequest requestDto) {
verifyEmailCode(requestDto.code(), requestDto.email());
final Member member = memberRepository.findByEmailAndIsDeletedTrue(requestDto.email())
.orElseThrow(() -> new MemberException(MEMBER_NOT_FOUND));
member.updateToActive();
return member.getId();
}

private void verifyEmailCode(String code, String email) {
EmailVerifyCode emailVerifyCode = emailVerifyCodeRepository.findById(code)
.filter(result -> result.getEmail().equals(email))
.orElseThrow(() -> new MemberException(EMAIL_VERIFY_FAILED));
log.info("Email Restore Success: {}", emailVerifyCode.getEmail());
}

private void validatePassword(String plainPassword, String encodedPassword) {
Expand Down Expand Up @@ -113,4 +144,19 @@ private Member getAvailableMember(Long memberId) {
private String getCurrentAccessToken() {
return (String)SecurityContextHolder.getContext().getAuthentication().getCredentials();
}

private String createCode() {
int length = 6;
try {
Random random = SecureRandom.getInstanceStrong();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(random.nextInt(10));
}
return builder.toString();
} catch (NoSuchAlgorithmException e) {
throw new MemberException(CREATE_VERIFY_CODE_FAILED);
}
}

}
16 changes: 16 additions & 0 deletions api/api-member/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ spring:
profiles:
include: infra, security
active: dev
mail:
host: smtp.gmail.com
port: 587
username: [email protected]
password: gmail_password
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000

server:
port: 8081
Expand All @@ -20,3 +35,4 @@ logging:
sql: debug
orm:
jdbc.bind: trace

Loading

0 comments on commit ede4506

Please sign in to comment.