Skip to content

Commit

Permalink
Merge pull request #49 from saessagMarket/feat/#33-temporary-password…
Browse files Browse the repository at this point in the history
…-reset

비밀번호 찾기 및 변경 구현
  • Loading branch information
ahyeonkong authored Jan 23, 2025
2 parents 42f7319 + 3623c6f commit b4c2d78
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.market.saessag.domain.email.controller;

import com.market.saessag.domain.email.dto.EmailRequest;
import com.market.saessag.domain.email.dto.PasswordChangeRequest;
import com.market.saessag.domain.email.service.PasswordService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/password")
public class PasswordController {
private final PasswordService passwordService;

@PostMapping("/find")
public ResponseEntity<?> findPassword(@RequestBody @Validated EmailRequest request) {
// 이메일로 임시 비밀번호 발급
passwordService.sendTemporaryPassword(request.getEmail());
return ResponseEntity.ok("임시 비밀번호가 이메일로 발송되었습니다."); // 응답 포맷 통일 필요함
}

@PatchMapping("/change")
public ResponseEntity<?> changePassword(@RequestBody @Validated PasswordChangeRequest request) {
passwordService.changePassword(request);
return ResponseEntity.ok("비밀번호가 성공적으로 변경되었습니다."); // 응답 포맷 통일 필요함
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class EmailRequest { // 이메일 인증 요청 DTO
public class EmailRequest {
private String email; // 인증을 요청할 이메일 주소

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.market.saessag.domain.email.dto;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PasswordChangeRequest {
private String email;
private String currentPassword;
private String newPassword; // 새로 설정할 비밀번호
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void sendVerificationEmail(String toEmail) {

} catch (MessagingException e) { // 이메일 발송 실패 시 저장된 인증 정보 제거
verificationStore.remove(toEmail);
throw new CustomException(ErrorCode.INVALID_VERIFICATION_CODE); // 이메일 발송 실패
throw new CustomException(ErrorCode.EMAIL_SEND_FAILED); // 이메일 발송 실패
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.market.saessag.domain.email.service;

import com.market.saessag.domain.email.dto.PasswordChangeRequest;
import com.market.saessag.domain.user.entity.User;
import com.market.saessag.domain.user.repository.UserRepository;
import com.market.saessag.global.exception.CustomException;
import com.market.saessag.global.exception.ErrorCode;
import com.market.saessag.global.util.TemporaryPassword;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

@Service
@RequiredArgsConstructor
public class PasswordService {
private final UserRepository userRepository;
private final JavaMailSender mailSender; // 이메일 발송을 위한 스프링 제공 인터페이스
private final BCryptPasswordEncoder passwordEncoder;
private final Map<String, TemporaryPassword> temporaryPasswordStore = new ConcurrentHashMap<>(); // 임시 저장소 추가


// 임시 비밀번호 발급
public void sendTemporaryPassword(String email) {
try {
// 1. 사용자 존재 여부 확인
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// 2. 임시 비밀번호 생성
String temporaryPassword = generateTemporaryPassword();

// 3. 임시 비밀번호 저장 (5분 유효)
LocalDateTime expirationTime = LocalDateTime.now().plusMinutes(5);
TemporaryPassword tempPassword = new TemporaryPassword(temporaryPassword, expirationTime);
temporaryPasswordStore.put(email, tempPassword);

// 4. 이메일 발송
sendPasswordEmail(email, temporaryPassword);

// 5. 이메일 발송 성공 시 DB 업데이트
user.updatePassword(passwordEncoder.encode(temporaryPassword));
userRepository.save(user);

} catch (CustomException e) {
temporaryPasswordStore.remove(email);
throw new CustomException(ErrorCode.EMAIL_SEND_FAILED);
}
}

// 임시 비밀번호 생성 (10자리)
private String generateTemporaryPassword() {
StringBuilder password = new StringBuilder();
Random random = new Random();

// 대문자, 소문자, 숫자 포함
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

for (int i = 0; i < 10; i++) {
password.append(chars.charAt(random.nextInt(chars.length())));
}

return password.toString();
}

private void sendPasswordEmail(String email, String temporaryPassword) {
try {
// 이메일 메시지 생성
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);

helper.setTo(email);
helper.setSubject("[새싹마켓] 임시 비밀번호 발급");
helper.setText(createEmailContent(temporaryPassword), true);

// 이메일 발송
mailSender.send(message);
} catch (MessagingException e) {// 이메일 발송 실패 시 임시 저장소에서 제거
temporaryPasswordStore.remove(email);
throw new CustomException(ErrorCode.EMAIL_SEND_FAILED); // 이메일 발송 실패
}
}

private String createEmailContent(String temporaryPassword) {
return String.format("""
<div style='text-align: center; margin: 30px;'>
<h3> saessagMarket </h3>
<h2>임시 비밀번호 발급</h2>
<p> 본 메일은 saessagMarket 임시 비밀번호 발급을 위한 이메일입니다.</p>
<p>아래의 임시 비밀번호로 로그인해 주세요.</p>
<p>보안을 위해 로그인 후 비밀번호를 변경해 주세요.</p>
<p style='font-size: 24px; font-weight: bold; margin: 20px;'>%s</p>
</div>
""", temporaryPassword);
}

// 5분마다 만료된 임시 비밀번호 정리
@Scheduled(fixedRate = 300000)
public void cleanupExpiredPasswords() {
temporaryPasswordStore.entrySet().removeIf(entry -> entry.getValue().isExpired());
}

// 비밀번호 변경
@Transactional
public void changePassword(PasswordChangeRequest request) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

// 현재 비밀번호 확인
if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD);
}

// 새 비밀번호 암호화 및 업데이트
user.updatePassword(passwordEncoder.encode(request.getNewPassword()));
userRepository.save(user);
}

}
5 changes: 5 additions & 0 deletions src/main/java/com/market/saessag/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ public User(String email, String password, String profileUrl, String nickname, S
this.updatedAt = LocalDateTime.now();
}

public void updatePassword(String newPassword) {
this.password = newPassword;
this.updatedAt = LocalDateTime.now();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ public enum ErrorCode {
DUPLICATE_EMAIL(400, "이미 가입된 이메일입니다"),
VERIFICATION_EXPIRED(400, "인증 시간이 만료되었습니다"),
INVALID_VERIFICATION_CODE(400, "잘못된 인증 코드입니다"),
EMAIL_NOT_VERIFIED(400, "이메일 인증이 필요합니다. 먼저 이메일 인증을 완료해주세요.");
EMAIL_NOT_VERIFIED(400, "이메일 인증이 필요합니다. 먼저 이메일 인증을 완료해주세요."),
USER_NOT_FOUND(404, "존재하지 않는 사용자입니다"),
EMAIL_SEND_FAILED(500, "이메일 발송에 실패했습니다"),
INVALID_EMAIL(400, "유효하지 않은 이메일입니다"),
INVALID_PASSWORD(400, "현재 비밀번호가 일치하지 않습니다");

private final int status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.market.saessag.global.exception;

import com.market.saessag.global.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.market.saessag.global.exception;
package com.market.saessag.global.response;

import com.market.saessag.global.exception.ErrorCode;
import lombok.*;

@Getter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.market.saessag.global.util;

import lombok.Getter;
import java.time.LocalDateTime;

@Getter
public class TemporaryPassword {
private final String password; // 생성된 임시 비밀번호
private final LocalDateTime expirationTime; // 만료 시간

public TemporaryPassword(String password, LocalDateTime expirationTime) {
this.password = password;
this.expirationTime = expirationTime;
}

public boolean isExpired() {
return LocalDateTime.now().isAfter(expirationTime);
}
}

0 comments on commit b4c2d78

Please sign in to comment.