-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 비밀번호 변경 로직 구현 #342
feat: 비밀번호 변경 로직 구현 #342
Changes from all commits
9de105e
8c1d129
11adf76
497ecfa
96e19b3
75df5d1
2658e0a
b9dc0cb
6b22509
66e0b1f
6902e33
9c95dcd
bbfead4
2dd5707
5294f68
ca6e927
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package in.koreatech.koin.domain.user.dto; | ||
|
||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record FindPasswordRequest( | ||
@Email(message = "아우누리 계정 형식이 아닙니다. ${validatedValue}") | ||
@NotNull(message = "이메일은 비어있을 수 없습니다.") | ||
@Schema(description = "이메일 주소", requiredMode = REQUIRED, example = "asdf@koreatech.ac.kr") | ||
@JsonProperty(value = "address") | ||
String email | ||
) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package in.koreatech.koin.domain.user.dto; | ||
|
||
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
|
||
@JsonNaming(SnakeCaseStrategy.class) | ||
public record UserPasswordChangeRequest( | ||
String password | ||
) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package in.koreatech.koin.domain.user.exception; | ||
|
||
import in.koreatech.koin.global.auth.exception.AuthenticationException; | ||
|
||
public class UserResetTokenExpiredException extends AuthenticationException { | ||
|
||
private static final String DEFAULT_MESSAGE = "비밀번호 재설정 토큰이 만료되었습니다."; | ||
|
||
public UserResetTokenExpiredException(String message) { | ||
super(message); | ||
} | ||
|
||
public static UserResetTokenExpiredException withDetail(String detail) { | ||
String message = String.format("%s %s", DEFAULT_MESSAGE, detail); | ||
return new UserResetTokenExpiredException(message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,14 @@ | |
|
||
import static lombok.AccessLevel.PROTECTED; | ||
|
||
import java.time.Clock; | ||
import java.time.LocalDateTime; | ||
|
||
import org.hibernate.annotations.SQLDelete; | ||
import org.hibernate.annotations.Where; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
|
||
import in.koreatech.koin.domain.user.exception.UserResetTokenExpiredException; | ||
import in.koreatech.koin.global.config.LocalDateTimeAttributeConverter; | ||
import in.koreatech.koin.global.domain.BaseEntity; | ||
import jakarta.persistence.Column; | ||
|
@@ -33,6 +35,7 @@ | |
@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?") | ||
@NoArgsConstructor(access = PROTECTED) | ||
public class User extends BaseEntity { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
@@ -70,7 +73,7 @@ public class User extends BaseEntity { | |
|
||
@NotNull | ||
@Column(name = "is_authed", nullable = false) | ||
private Boolean isAuthed = false; | ||
private boolean isAuthed = false; | ||
|
||
@Column(name = "last_logged_at") | ||
private LocalDateTime lastLoggedAt; | ||
|
@@ -87,26 +90,27 @@ public class User extends BaseEntity { | |
@Column(name = "auth_token") | ||
private String authToken; | ||
|
||
@Column(name = "auth_expired_at") | ||
@Convert(converter = LocalDateTimeAttributeConverter.class) | ||
@Column(name = "auth_expired_at") | ||
private LocalDateTime authExpiredAt; | ||
|
||
@Size(max = 255) | ||
@Column(name = "reset_token") | ||
private String resetToken; | ||
|
||
@Column(name = "reset_expired_at") | ||
@Convert(converter = LocalDateTimeAttributeConverter.class) | ||
@Column(name = "reset_expired_at") | ||
private LocalDateTime resetExpiredAt; | ||
|
||
@Column(name = "device_token", nullable = true) | ||
private String deviceToken; | ||
|
||
@Builder | ||
private User(String password, String nickname, String name, String phoneNumber, UserType userType, | ||
String email, UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, | ||
Boolean isDeleted, String authToken, LocalDateTime authExpiredAt, String resetToken, LocalDateTime resetExpiredAt, | ||
String deviceToken) { | ||
String email, UserGender gender, boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, | ||
Boolean isDeleted, String authToken, LocalDateTime authExpiredAt, String resetToken, | ||
LocalDateTime resetExpiredAt, | ||
String deviceToken) { | ||
this.password = password; | ||
this.nickname = nickname; | ||
this.name = name; | ||
|
@@ -145,6 +149,11 @@ public void updatePassword(PasswordEncoder passwordEncoder, String password) { | |
this.password = passwordEncoder.encode(password); | ||
} | ||
|
||
public void generateResetTokenForFindPassword(Clock clock) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AClock을 사용하는 것은 처음보는데, 꽤나 유용하네요 :O There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드리뷰 많이 하고다니시죠 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 죄송합니다 . . . . 분발하겠습니다 |
||
this.resetExpiredAt = LocalDateTime.now(clock).plusHours(1); | ||
this.resetToken = this.email + this.resetExpiredAt; | ||
} | ||
|
||
public void update(String nickname, String name, String phoneNumber, UserGender gender) { | ||
this.nickname = nickname; | ||
this.name = name; | ||
|
@@ -155,4 +164,10 @@ public void update(String nickname, String name, String phoneNumber, UserGender | |
public void auth() { | ||
this.isAuthed = true; | ||
} | ||
|
||
public void validateResetToken() { | ||
if (resetExpiredAt.isBefore(LocalDateTime.now())) { | ||
throw UserResetTokenExpiredException.withDetail("resetToken: " + resetToken); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,10 +11,12 @@ | |
import org.springframework.web.servlet.ModelAndView; | ||
|
||
import in.koreatech.koin.domain.user.dto.AuthTokenRequest; | ||
import in.koreatech.koin.domain.user.dto.FindPasswordRequest; | ||
import in.koreatech.koin.domain.user.dto.StudentRegisterRequest; | ||
import in.koreatech.koin.domain.user.dto.StudentResponse; | ||
import in.koreatech.koin.domain.user.dto.StudentUpdateRequest; | ||
import in.koreatech.koin.domain.user.dto.StudentUpdateResponse; | ||
import in.koreatech.koin.domain.user.dto.UserPasswordChangeRequest; | ||
import in.koreatech.koin.domain.user.exception.DuplicationNicknameException; | ||
import in.koreatech.koin.domain.user.exception.StudentDepartmentNotValidException; | ||
import in.koreatech.koin.domain.user.exception.StudentNumberNotValidException; | ||
|
@@ -27,6 +29,7 @@ | |
import in.koreatech.koin.domain.user.repository.StudentRepository; | ||
import in.koreatech.koin.domain.user.repository.UserRepository; | ||
import in.koreatech.koin.global.domain.email.exception.DuplicationEmailException; | ||
import in.koreatech.koin.global.domain.email.form.StudentPasswordChangeData; | ||
import in.koreatech.koin.global.domain.email.form.StudentRegistrationData; | ||
import in.koreatech.koin.global.domain.email.model.EmailAddress; | ||
import in.koreatech.koin.global.domain.email.service.MailService; | ||
|
@@ -121,10 +124,33 @@ private void validateStudentNumber(String studentNumber) { | |
if (studentNumber == null) { | ||
return; | ||
} | ||
Integer studentNumberYear = Student.parseStudentNumberYear(studentNumber); | ||
int studentNumberYear = Student.parseStudentNumberYear(studentNumber); | ||
if (studentNumberYear < 1992 | ||
|| new LocalDateTime().now().getYear() < studentNumberYear) { | ||
|| LocalDateTime.now().getYear() < studentNumberYear) { | ||
throw StudentNumberNotValidException.withDetail("studentNumber: " + studentNumber); | ||
} | ||
} | ||
|
||
@Transactional | ||
public void findPassword(FindPasswordRequest request, String serverURL) { | ||
User user = userRepository.getByEmail(request.email()); | ||
user.generateResetTokenForFindPassword(clock); | ||
User authedUser = userRepository.save(user); | ||
mailService.sendMail(request.email(), new StudentPasswordChangeData(serverURL, authedUser.getResetToken())); | ||
} | ||
|
||
public ModelAndView checkResetToken(String resetToken, String serverUrl) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AModelAndView를 사용 방법을 잘 익혀야겠습니다. |
||
ModelAndView modelAndView = new ModelAndView("change_password_config"); | ||
modelAndView.addObject("contextPath", serverUrl); | ||
modelAndView.addObject("resetToken", resetToken); | ||
return modelAndView; | ||
} | ||
|
||
@Transactional | ||
public void changePassword(UserPasswordChangeRequest request, String resetToken) { | ||
User authedUser = userRepository.getByResetToken(resetToken); | ||
authedUser.validateResetToken(); | ||
authedUser.updatePassword(passwordEncoder, request.password()); | ||
userRepository.save(authedUser); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C
개행 한 줄 지워주세요!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵