Skip to content

Commit 1122577

Browse files
authored
Merge pull request #14 from KONKUK-MAP-Service/feat-login
➕ [Feat] : 메일로 임시 비밀번호 발급 기능 구현 #14
2 parents a20e13c + 63ce580 commit 1122577

20 files changed

+302
-3
lines changed

.github/workflows/CD.yml

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ jobs:
3939
spring.jwt.secret: ${{ secrets.JWT_SECRET }}
4040
spring.data.redis.host: ${{secrets.REDIS_HOST}}
4141
spring.data.redis.port: ${{secrets.REDIS_PORT}}
42+
spring.mail.username: ${{secrets.MAIL_USERNAME}}
43+
spring.mail.password: ${{secrets.MAIL_PASSWORD}}
4244

4345
- name: Grant execute permission for gradlew
4446
run: chmod +x ./gradlew

.github/workflows/CI.yml

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
spring.jwt.secret: ${{ secrets.JWT_SECRET }}
4242
spring.data.redis.host: ${{secrets.REDIS_HOST}}
4343
spring.data.redis.port: ${{secrets.REDIS_PORT}}
44+
spring.mail.username: ${{secrets.MAIL_USERNAME}}
45+
spring.mail.password: ${{secrets.MAIL_PASSWORD}}
46+
4447

4548

4649
#gradlew 실행을 위한 권한 추가

build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ dependencies {
5151

5252
//redis 의존성 추가
5353
implementation("org.springframework.boot:spring-boot-starter-data-redis")
54+
//SMTP
55+
implementation 'org.springframework.boot:spring-boot-starter-mail'
5456

5557
}
5658

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.cona.KUsukKusuk.email;
2+
3+
import java.util.Properties;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.mail.javamail.JavaMailSender;
8+
import org.springframework.mail.javamail.JavaMailSenderImpl;
9+
@Configuration
10+
public class EmailConfig {
11+
@Value("${spring.mail.host}")
12+
private String host;
13+
14+
@Value("${spring.mail.port}")
15+
private int port;
16+
17+
@Value("${spring.mail.username}")
18+
private String username;
19+
20+
@Value("${spring.mail.password}")
21+
private String password;
22+
23+
@Value("${spring.mail.properties.mail.smtp.auth}")
24+
private boolean auth;
25+
26+
@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
27+
private boolean starttlsEnable;
28+
29+
@Value("${spring.mail.properties.mail.smtp.starttls.required}")
30+
private boolean starttlsRequired;
31+
32+
@Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
33+
private int connectionTimeout;
34+
35+
@Value("${spring.mail.properties.mail.smtp.timeout}")
36+
private int timeout;
37+
38+
@Value("${spring.mail.properties.mail.smtp.writetimeout}")
39+
private int writeTimeout;
40+
41+
@Bean
42+
public JavaMailSender javaMailSender() {
43+
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
44+
mailSender.setHost(host);
45+
mailSender.setPort(port);
46+
mailSender.setUsername(username);
47+
mailSender.setPassword(password);
48+
mailSender.setDefaultEncoding("UTF-8");
49+
mailSender.setJavaMailProperties(getMailProperties());
50+
51+
return mailSender;
52+
}
53+
54+
private Properties getMailProperties() {
55+
Properties properties = new Properties();
56+
properties.put("mail.smtp.auth", auth);
57+
properties.put("mail.smtp.starttls.enable", starttlsEnable);
58+
properties.put("mail.smtp.starttls.required", starttlsRequired);
59+
properties.put("mail.smtp.connectiontimeout", connectionTimeout);
60+
properties.put("mail.smtp.timeout", timeout);
61+
properties.put("mail.smtp.writetimeout", writeTimeout);
62+
63+
return properties;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.cona.KUsukKusuk.email.dto;
2+
3+
public record EmailPostDto(String email) {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.cona.KUsukKusuk.email.dto;
2+
3+
public record EmailResponseDto(boolean success) {
4+
public boolean isSuccess() {
5+
return success;
6+
}
7+
public static EmailResponseDto of(boolean success) {
8+
return new EmailResponseDto(success);
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.cona.KUsukKusuk.email.dto;
2+
3+
public record EmailSendSuccessResponse(String message) {
4+
public static EmailSendSuccessResponse of(String message) {
5+
return new EmailSendSuccessResponse(message+"로 인증코드가 성공적으로 전송되었습니다. 유효기간은 30분 입니다.");
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.cona.KUsukKusuk.email.dto;
2+
3+
public record EmailVerifySuccessResponse(String message) {
4+
5+
public static EmailVerifySuccessResponse of(Boolean success) {
6+
if (success) {
7+
return new EmailVerifySuccessResponse("이메일 인증이 성공적으로 완료되었습니다.");
8+
}
9+
else {
10+
return new EmailVerifySuccessResponse("인증코드가 올바르지 않습니다.");
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.cona.KUsukKusuk.email.dto;
2+
3+
import lombok.Builder;
4+
5+
@Builder
6+
public record LoginRequest( String username, String password) {
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.cona.KUsukKusuk.email.exception;
2+
3+
import com.cona.KUsukKusuk.global.exception.HttpExceptionCode;
4+
import lombok.Getter;
5+
import lombok.Setter;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.http.HttpStatus;
8+
9+
@Getter
10+
@Setter
11+
@Slf4j
12+
public class EmailNotSendException extends RuntimeException{
13+
private final HttpStatus httpStatus;
14+
15+
public EmailNotSendException(HttpExceptionCode exceptionCode) {
16+
super(exceptionCode.getMessage());
17+
this.httpStatus = exceptionCode.getHttpStatus();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.cona.KUsukKusuk.email.exception.handler;
2+
3+
4+
import com.cona.KUsukKusuk.email.exception.EmailNotSendException;
5+
import com.cona.KUsukKusuk.global.response.ErrorResponse;
6+
import com.cona.KUsukKusuk.global.response.HttpResponse;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.core.Ordered;
9+
import org.springframework.core.annotation.Order;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.web.bind.annotation.ExceptionHandler;
12+
import org.springframework.web.bind.annotation.ResponseStatus;
13+
import org.springframework.web.bind.annotation.RestControllerAdvice;
14+
15+
@Slf4j
16+
@RestControllerAdvice
17+
@Order(Ordered.HIGHEST_PRECEDENCE)
18+
public class EmailExceptionHandler {
19+
@ExceptionHandler(EmailNotSendException.class)
20+
@ResponseStatus(HttpStatus.BAD_REQUEST)
21+
public HttpResponse<ErrorResponse> EmailnotSendExceptionHandler(EmailNotSendException e) {
22+
return HttpResponse.status(e.getHttpStatus())
23+
.body(ErrorResponse.from(e.getHttpStatus(), e.getMessage()));
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.cona.KUsukKusuk.email.service;
2+
3+
import com.cona.KUsukKusuk.email.exception.EmailNotSendException;
4+
import jakarta.transaction.Transactional;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.mail.SimpleMailMessage;
8+
import org.springframework.mail.javamail.JavaMailSender;
9+
import org.springframework.stereotype.Service;
10+
import com.cona.KUsukKusuk.global.exception.HttpExceptionCode;
11+
12+
@Slf4j
13+
@Service
14+
@Transactional
15+
@RequiredArgsConstructor
16+
public class EmailService {
17+
18+
private final JavaMailSender emailSender;
19+
20+
public void sendEmail(String toEmail,
21+
String title,
22+
String text) {
23+
SimpleMailMessage emailForm = createEmailForm(toEmail, title, text);
24+
try {
25+
emailSender.send(emailForm);
26+
} catch (RuntimeException e) {
27+
28+
throw new EmailNotSendException(HttpExceptionCode.EMAIL_NOT_SEND);
29+
}
30+
}
31+
32+
33+
private SimpleMailMessage createEmailForm(String toEmail,
34+
String title,
35+
String text) {
36+
SimpleMailMessage message = new SimpleMailMessage();
37+
message.setTo(toEmail);
38+
message.setSubject(title);
39+
message.setText(text);
40+
41+
return message;
42+
}
43+
44+
}

src/main/java/com/cona/KUsukKusuk/global/exception/HttpExceptionCode.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ public enum HttpExceptionCode {
1616
JWT_NOT_FOUND(HttpStatus.UNAUTHORIZED, "JWT를 찾을 수 없습니다."),
1717
REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "리프레시 토큰을 찾을 수 없습니다."),
1818

19-
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."); // 새로운 예외 코드 추가
19+
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
20+
21+
EMAIL_NOT_SEND(HttpStatus.NOT_FOUND,"이메일이 전송에 실패하였습니다.");
2022

2123

2224
private final HttpStatus httpStatus;

src/main/java/com/cona/KUsukKusuk/global/security/JWTUtil.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.jsonwebtoken.Jwts;
66
import jakarta.servlet.http.HttpServletRequest;
77
import java.nio.charset.StandardCharsets;
8+
import java.time.Duration;
89
import java.util.Date;
910
import java.util.Map;
1011
import java.util.concurrent.TimeUnit;
@@ -73,7 +74,8 @@ public String createRefreshToken(String userid, String password, Long expiredMs)
7374
.compact();
7475

7576
// redis에 RT저장
76-
redisService.setValues(refreshToken,userid);
77+
redisService.setValues(refreshToken, userid, Duration.ofMillis(expiredMs));
78+
7779

7880
return refreshToken;
7981
}
@@ -96,4 +98,5 @@ public String getRefreshToken(HttpServletRequest request) {
9698
}
9799

98100

101+
99102
}

src/main/java/com/cona/KUsukKusuk/user/controller/UserController.java

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.cona.KUsukKusuk.global.response.HttpResponse;
44
import com.cona.KUsukKusuk.global.security.JWTUtil;
55
import com.cona.KUsukKusuk.user.domain.User;
6+
import com.cona.KUsukKusuk.user.dto.FindPasswordRequest;
7+
import com.cona.KUsukKusuk.user.dto.FindPasswordResponse;
68
import com.cona.KUsukKusuk.user.dto.TokenRefreshRequest;
79
import com.cona.KUsukKusuk.user.dto.TokenRefreshResponse;
810
import com.cona.KUsukKusuk.user.dto.UserJoinRequest;
@@ -62,4 +64,12 @@ public HttpResponse<TokenRefreshResponse> refreshToken(@RequestBody TokenRefresh
6264
TokenRefreshResponse.of(newAccessToken)
6365
);
6466
}
67+
@PostMapping("/find-password")
68+
@Operation(summary = "비밀번호 찾기", description = "아이디와 이메일로 임시 비밀번호를 발급합니다.")
69+
public HttpResponse<FindPasswordResponse> findPassword(@Valid @RequestBody FindPasswordRequest findPasswordRequest) {
70+
String email = userService.findPassword(findPasswordRequest.userId(), findPasswordRequest.email());
71+
return HttpResponse.okBuild(
72+
FindPasswordResponse.of(email)
73+
);
74+
}
6575
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.cona.KUsukKusuk.user.dto;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
6+
public record FindPasswordRequest(
7+
@NotBlank(message = "사용자 이름은 필수 입력값입니다.")
8+
String userId,
9+
10+
@NotBlank(message = "이메일 값은 필수 입력 값입니다.")
11+
@Email(message = "이메일 형식에 맞지 않습니다.")
12+
String email
13+
) {
14+
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.cona.KUsukKusuk.user.dto;
2+
3+
import lombok.Builder;
4+
5+
@Builder
6+
public record FindPasswordResponse(String message) {
7+
8+
public static FindPasswordResponse of(String email) {
9+
10+
11+
return FindPasswordResponse.builder()
12+
.message(email+"로 임시 비밀번호를 보냈습니다.")
13+
.build();
14+
15+
16+
}
17+
}

src/main/java/com/cona/KUsukKusuk/user/repository/UserRepository.java

+2
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
1313

1414
Optional<User> findByUserId(String userid);
1515

16+
Optional<User> findByUserIdAndEmail(String userId, String email);
17+
1618
}

src/main/java/com/cona/KUsukKusuk/user/service/UserService.java

+32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.cona.KUsukKusuk.user.service;
22

3+
import com.cona.KUsukKusuk.email.service.EmailService;
34
import com.cona.KUsukKusuk.global.exception.HttpExceptionCode;
45
import com.cona.KUsukKusuk.global.exception.custom.security.SecurityJwtNotFoundException;
56
import com.cona.KUsukKusuk.global.redis.RedisService;
@@ -9,6 +10,8 @@
910
import com.cona.KUsukKusuk.user.exception.UserNotFoundException;
1011
import com.cona.KUsukKusuk.user.repository.UserRepository;
1112
import lombok.RequiredArgsConstructor;
13+
import org.apache.commons.lang3.RandomStringUtils;
14+
import org.springframework.security.core.context.SecurityContextHolder;
1215
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
1316
import org.springframework.stereotype.Service;
1417

@@ -19,6 +22,7 @@ public class UserService {
1922
private final BCryptPasswordEncoder bCryptPasswordEncoder;
2023

2124
private final RedisService redisService;
25+
private final EmailService mailService;
2226
private final JWTUtil jwtUtil;
2327

2428
public User save(UserJoinRequest userJoinRequest) {
@@ -74,6 +78,34 @@ private void isTokenPresent(String encryptedRefreshToken) {
7478
throw new SecurityJwtNotFoundException(HttpExceptionCode.JWT_NOT_FOUND);
7579
}
7680
}
81+
public String findPassword(String userId, String email) {
82+
User member = userRepository.findByUserIdAndEmail(userId, email)
83+
.orElseThrow(() -> new UserNotFoundException(HttpExceptionCode.USER_NOT_FOUND));
7784

85+
String newPassword = generateNewPassword();
86+
member.setPassword(bCryptPasswordEncoder.encode(newPassword));
87+
userRepository.save(member);
88+
89+
String title = "쿠석쿠석 임시 비밀번호 발급";
90+
mailService.sendEmail(email, title, "새로운 비밀번호 : " + newPassword);
91+
92+
return email;
93+
}
94+
private String generateNewPassword() {
95+
String randomString = RandomStringUtils.randomAlphanumeric(9);
96+
97+
int randomIndex = (int) (Math.random() * 9);
98+
99+
char randomNumber = (char) ('0' + (int) (Math.random() * 10));
100+
char[] newPasswordChars = randomString.toCharArray();
101+
newPasswordChars[randomIndex] = randomNumber;
102+
103+
return new String(newPasswordChars);
104+
}
105+
106+
public String getUsernameBySecurityContext() {
107+
return SecurityContextHolder.getContext().getAuthentication()
108+
.getName();
109+
}
78110

79111
}

0 commit comments

Comments
 (0)