Skip to content

Commit

Permalink
feat: 비밀번호 변경 로직 구현 (#342)
Browse files Browse the repository at this point in the history
* feat : 수정을 위한 1차 커밋

* feat : mailForm 생성 및 DTO, mail html 추가

* feat: controller 작성

* feat: webConfig 추가

* feat: resetToken전달하도록 수정

* feat: 이메일 전송

* feat: 버튼 누를 때 기능

* chore : 권한 static import

* chore : LocalDateTime 형식으로 수정 및 미사용 파일 삭제

* feat: 비밀번호 초기화 구현

* docs: hidden 추가

* style: 개행제거

---------

Co-authored-by: duehee <149302959+duehee@users.noreply.github.com>
Co-authored-by: daheeParkk <qkrekgml7414@naver.com>
  • Loading branch information
3 people authored Apr 8, 2024
1 parent 0806381 commit a64b66c
Show file tree
Hide file tree
Showing 22 changed files with 378 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.domain.coop.dto.DiningImageRequest;
import org.springframework.web.bind.annotation.RequestMapping;

import in.koreatech.koin.domain.coop.dto.SoldOutRequest;
import in.koreatech.koin.domain.coop.service.CoopService;
import in.koreatech.koin.global.auth.Auth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import in.koreatech.koin.domain.user.dto.AuthResponse;
import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.FindPasswordRequest;
import in.koreatech.koin.domain.user.dto.NicknameCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.StudentRegisterRequest;
import in.koreatech.koin.domain.user.dto.StudentResponse;
Expand Down Expand Up @@ -187,4 +188,20 @@ ResponseEntity<Void> checkDuplicationOfNickname(
ResponseEntity<AuthResponse> getAuth(
@Auth(permit = {STUDENT, OWNER, COOP}) Long userId
);

@ApiResponses(
value = {
@ApiResponse(responseCode = "201"),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
}
)
@Operation(summary = "비밀번호 초기(변경) 메일 발송")
@PostMapping("/user/find/password")
ResponseEntity<Void> findPassword(
@RequestBody @Valid FindPasswordRequest findPasswordRequest,
@ServerURL String serverURL
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,37 @@

import java.net.URI;

import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import in.koreatech.koin.domain.user.dto.AuthTokenRequest;
import in.koreatech.koin.domain.user.dto.AuthResponse;
import in.koreatech.koin.domain.user.dto.AuthTokenRequest;
import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.FindPasswordRequest;
import in.koreatech.koin.domain.user.dto.NicknameCheckExistsRequest;
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.UserLoginRequest;
import in.koreatech.koin.domain.user.dto.UserLoginResponse;
import in.koreatech.koin.domain.user.dto.UserPasswordChangeRequest;
import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest;
import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse;

import in.koreatech.koin.domain.user.service.StudentService;
import in.koreatech.koin.domain.user.service.UserService;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.host.ServerURL;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand Down Expand Up @@ -105,7 +109,8 @@ public ResponseEntity<Void> checkUserEmailExist(
@PostMapping("/user/student/register")
public ResponseEntity<Void> studentRegister(
@Valid @RequestBody StudentRegisterRequest request,
@ServerURL String serverURL) {
@ServerURL String serverURL
) {
studentService.studentRegister(request, serverURL);
return ResponseEntity.ok().build();
}
Expand Down Expand Up @@ -134,4 +139,31 @@ public ResponseEntity<AuthResponse> getAuth(
AuthResponse authResponse = userService.getAuth(userId);
return ResponseEntity.ok().body(authResponse);
}

@PostMapping("/user/find/password")
public ResponseEntity<Void> findPassword(
@RequestBody @Valid FindPasswordRequest request,
@ServerURL String serverURL
) {
studentService.findPassword(request, serverURL);
return new ResponseEntity<>(HttpStatusCode.valueOf(201));
}

@GetMapping("/user/change/password/config")
public ModelAndView checkResetToken(
@ServerURL String serverUrl,
@RequestParam("reset_token") String resetToken
) {
return studentService.checkResetToken(resetToken, serverUrl);
}

@Hidden
@PostMapping("/user/change/password/submit")
public ResponseEntity<Void> changePassword(
@RequestBody UserPasswordChangeRequest request,
@RequestParam("reset_token") String resetToken
) {
studentService.changePassword(request, resetToken);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package in.koreatech.koin.domain.user.dto;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*;
import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

Expand Down
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
@@ -1,6 +1,6 @@
package in.koreatech.koin.domain.user.dto;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*;
import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;

import java.time.Clock;
import java.time.LocalDateTime;
Expand Down
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
Expand Up @@ -24,7 +24,7 @@ public ModelAndView toModelAndViewForStudent() {
if (user.getAuthExpiredAt().isBefore(LocalDateTime.now(clock))) {
return createErrorModelAndView("이미 만료된 토큰입니다.");
}
if (!user.getIsAuthed()) {
if (!user.isAuthed()) {
user.auth();
eventPublisher.publishEvent(new StudentRegisterEvent(user.getEmail()));
return createSuccessModelAndView();
Expand Down
27 changes: 21 additions & 6 deletions src/main/java/in/koreatech/koin/domain/user/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -145,6 +149,11 @@ public void updatePassword(PasswordEncoder passwordEncoder, String password) {
this.password = passwordEncoder.encode(password);
}

public void generateResetTokenForFindPassword(Clock clock) {
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;
Expand All @@ -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
Expand Up @@ -19,6 +19,8 @@ public interface UserRepository extends Repository<User, Long> {

Optional<User> findByAuthToken(String authToken);

Optional<User> findAllByResetToken(String resetToken);

default User getByEmail(String email) {
return findByEmail(email)
.orElseThrow(() -> UserNotFoundException.withDetail("email: " + email));
Expand All @@ -34,6 +36,11 @@ default User getByNickname(String nickname) {
.orElseThrow(() -> UserNotFoundException.withDetail("nickname: " + nickname));
}

default User getByResetToken(String resetToken) {
return findAllByResetToken(resetToken)
.orElseThrow(() -> UserNotFoundException.withDetail("resetToken: " + resetToken));
}

boolean existsByNickname(String nickname);

void delete(User user);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ public class LocalDateTimeAttributeConverter implements AttributeConverter<Local

@Override
public String convertToDatabaseColumn(LocalDateTime localDateTime) {
if (localDateTime == null)
if (localDateTime == null) {
return null;
}
return localDateTime.format(formatter);
}

@Override
public LocalDateTime convertToEntityAttribute(String dbData) {
if (dbData == null)
if (dbData == null) {
return null;
}
return LocalDateTime.parse(dbData, formatter);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package in.koreatech.koin.global.config;


import java.util.List;

import org.springframework.context.annotation.Configuration;
Expand Down
Loading

0 comments on commit a64b66c

Please sign in to comment.