diff --git a/src/main/java/com/eactive/testmaster/login/controller/PasswordResetController.java b/src/main/java/com/eactive/testmaster/login/controller/PasswordResetController.java new file mode 100644 index 0000000..0be0d09 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/login/controller/PasswordResetController.java @@ -0,0 +1,108 @@ +package com.eactive.testmaster.login.controller; + +import com.eactive.testmaster.common.exception.UserNotFoundException; +import com.eactive.testmaster.login.dto.PasswordResetDTO; +import com.eactive.testmaster.login.entity.Token; +import com.eactive.testmaster.login.service.TokenService; +import com.eactive.testmaster.user.service.UserService; +import java.time.LocalDateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Validator; +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.RequestParam; + +/** + * Created by Sungpil Hyun + */ +@Controller +@ConditionalOnProperty(name = "password.reset.enabled", havingValue = "true") +public class PasswordResetController { + + public static final String RESET_PASSWORD = "page/resetPassword"; + public static final String FORGOT_PASSWORD = "page/forgotPassword"; + @Autowired + private UserService userService; + + @Autowired + private TokenService tokenService; + + @Autowired + private Validator validator; + + + @GetMapping(value = "/forgot_password.do") + public String forgotPasswordView() { + return FORGOT_PASSWORD; + } + + /** + * 비밀번호 초기화 링크를 생성한다. + * + * @param email + * @param model + * @return + */ + @PostMapping(value = "/forgot_password.do") + public String requestPasswordReset(@RequestParam(value = "email", defaultValue = "0") String email, + Model model) { + try { + userService.requestPasswordReset(email); + } catch (UserNotFoundException e) { + model.addAttribute("error", "존재하지 않는 이메일입니다."); + return FORGOT_PASSWORD; + } catch (Exception e) { + model.addAttribute("error", "비밀번호 초기화 요청에 실패하였습니다."); + return FORGOT_PASSWORD; + } + + return "redirect:/forgot_password.do?success"; + } + + @GetMapping("/reset_password.do") + public String resetPasswordView(@RequestParam(value = "token", defaultValue = "0") String tokenValue, + Model model) { + Token token = tokenService.findByToken(tokenValue); + + if (token == null || token.getExpiresAt().isBefore(LocalDateTime.now())) { + return "redirect:/forgot_password.do?expired"; + } + + model.addAttribute("tokenValue", tokenValue); + model.addAttribute("passwordReset", new PasswordResetDTO()); + return RESET_PASSWORD; + } + + @PostMapping("/reset_password.do") + @Transactional + public String resetPassword(@ModelAttribute("passwordResetRequest") PasswordResetDTO passwordReset, + BindingResult bindingResult, + Model model) { + + Token token = tokenService.findByToken(passwordReset.getToken()); + + if (token == null || token.getExpiresAt().isBefore(LocalDateTime.now())) { + return "redirect:/forgot_password.do?expired"; + } + + validator.validate(passwordReset, bindingResult); + + if (bindingResult.hasErrors()) { + model.addAttribute("tokenValue", passwordReset.getToken()); + model.addAttribute("passwordReset", passwordReset); + return RESET_PASSWORD; + } + + userService.resetPassword(token.getEmail(), passwordReset.getPassword()); + + tokenService.deleteToken(token); + + return "redirect:/login"; + } +} diff --git a/src/main/java/com/eactive/testmaster/login/dto/PasswordResetDTO.java b/src/main/java/com/eactive/testmaster/login/dto/PasswordResetDTO.java new file mode 100644 index 0000000..3475eac --- /dev/null +++ b/src/main/java/com/eactive/testmaster/login/dto/PasswordResetDTO.java @@ -0,0 +1,36 @@ +package com.eactive.testmaster.login.dto; + +import com.eactive.testmaster.common.validator.PasswordMatch; +import java.io.Serializable; + +@PasswordMatch(input = "password", confirm = "password2") +public class PasswordResetDTO implements Serializable { + + private String token; + private String password; + private String password2; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPassword2() { + return password2; + } + + public void setPassword2(String password2) { + this.password2 = password2; + } +} diff --git a/src/main/java/com/eactive/testmaster/login/entity/Token.java b/src/main/java/com/eactive/testmaster/login/entity/Token.java new file mode 100644 index 0000000..becd8a3 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/login/entity/Token.java @@ -0,0 +1,54 @@ +package com.eactive.testmaster.login.entity; + +import java.io.Serializable; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "PT_TOKEN") +public class Token implements Serializable { + + public Token(String tokenValue, String email, LocalDateTime expiresAt) { + this.tokenValue = tokenValue; + this.email = email; + this.expiresAt = expiresAt; + } + + public Token() { + } + + @Id + @Column(name = "TOKEN") + private String tokenValue; + + private String email; + + private LocalDateTime expiresAt; + + public String getTokenValue() { + return tokenValue; + } + + public void setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public LocalDateTime getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt; + } +} diff --git a/src/main/java/com/eactive/testmaster/login/repository/TokenRepository.java b/src/main/java/com/eactive/testmaster/login/repository/TokenRepository.java new file mode 100644 index 0000000..9d7fd9f --- /dev/null +++ b/src/main/java/com/eactive/testmaster/login/repository/TokenRepository.java @@ -0,0 +1,7 @@ +package com.eactive.testmaster.login.repository; + +import com.eactive.testmaster.login.entity.Token; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TokenRepository extends JpaRepository { +} diff --git a/src/main/java/com/eactive/testmaster/login/service/TokenService.java b/src/main/java/com/eactive/testmaster/login/service/TokenService.java new file mode 100644 index 0000000..a9a86ad --- /dev/null +++ b/src/main/java/com/eactive/testmaster/login/service/TokenService.java @@ -0,0 +1,32 @@ +package com.eactive.testmaster.login.service; + +import com.eactive.testmaster.login.entity.Token; +import com.eactive.testmaster.login.repository.TokenRepository; +import java.time.LocalDateTime; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class TokenService { + + @Autowired + private TokenRepository tokenRepository; + + public Token createToken(String email) { + String token = UUID.randomUUID().toString(); + LocalDateTime expiresAt = LocalDateTime.now().plusHours(24); + Token tokenObj = new Token(token, email, expiresAt); + return tokenRepository.save(tokenObj); + } + + public Token findByToken(String token) { + return tokenRepository.findById(token).orElse(null); + } + + public void deleteToken(Token token) { + tokenRepository.delete(token); + } +} diff --git a/src/main/java/com/eactive/testmaster/user/controller/UserMgmtController.java b/src/main/java/com/eactive/testmaster/user/controller/UserMgmtController.java index e1236f8..a5732e1 100644 --- a/src/main/java/com/eactive/testmaster/user/controller/UserMgmtController.java +++ b/src/main/java/com/eactive/testmaster/user/controller/UserMgmtController.java @@ -1,6 +1,7 @@ package com.eactive.testmaster.user.controller; import com.eactive.testmaster.common.util.SecurityUtil; +import com.eactive.testmaster.user.dto.StaffPasswordChangeRequestDTO; import com.eactive.testmaster.user.dto.UserRegisterDTO; import com.eactive.testmaster.user.dto.UserSearch; import com.eactive.testmaster.user.dto.UserUpdateDTO; @@ -25,8 +26,9 @@ import org.springframework.web.bind.annotation.RequestParam; @RequestMapping("/mgmt/users") public class UserMgmtController { - public static final String STAFF_USER_EDIT = "page/users/staffUserEdit"; - public static final String USERS_LIST_VIEW = "redirect:/mgmt/users/list_view.do"; + private static final String STAFF_USER_EDIT = "page/users/staffUserEdit"; + private static final String USERS_LIST_VIEW = "redirect:/mgmt/users/list_view.do"; + private static final String STAFF_PASSWORD_RESET_VIEW = "page/users/staffPasswordReset"; private final StaffUserService staffUserService; @@ -129,4 +131,23 @@ public class UserMgmtController { return USERS_LIST_VIEW; } + @PostMapping("/staff_password_reset_view.do") + public String staffPasswordReset(@ModelAttribute("passwordChangeRequest") StaffPasswordChangeRequestDTO passwordChangeRequestDTO) { + StaffUser found = staffUserService.findById(passwordChangeRequestDTO.getId()); + passwordChangeRequestDTO.setUsername(found.getUsername()); + return STAFF_PASSWORD_RESET_VIEW; + } + + @PostMapping("/update_staff_password.do") + public String updateStaffPassword(@ModelAttribute("passwordChangeRequest") StaffPasswordChangeRequestDTO passwordChangeRequestDTO, + BindingResult bindingResult) { + + validator.validate(passwordChangeRequestDTO, bindingResult); + if (bindingResult.hasErrors()) { + return STAFF_PASSWORD_RESET_VIEW; + } + staffUserService.updatePassword(passwordChangeRequestDTO.getId(), passwordChangeRequestDTO.getNewPassword()); + return USERS_LIST_VIEW; + } + } diff --git a/src/main/java/com/eactive/testmaster/user/dto/StaffPasswordChangeRequestDTO.java b/src/main/java/com/eactive/testmaster/user/dto/StaffPasswordChangeRequestDTO.java new file mode 100644 index 0000000..e2a35fc --- /dev/null +++ b/src/main/java/com/eactive/testmaster/user/dto/StaffPasswordChangeRequestDTO.java @@ -0,0 +1,51 @@ +package com.eactive.testmaster.user.dto; + +import com.eactive.testmaster.common.validator.PasswordMatch; +import javax.validation.constraints.NotEmpty; + +@PasswordMatch(input = "newPassword", confirm = "newPassword2") +public class StaffPasswordChangeRequestDTO { + + @NotEmpty(message = "{field.required}") + private String id; + + private String username; + + @NotEmpty(message = "{field.required}") + private String newPassword; + + @NotEmpty(message = "{field.required}") + private String newPassword2; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public String getNewPassword2() { + return newPassword2; + } + + public void setNewPassword2(String newPassword2) { + this.newPassword2 = newPassword2; + } +} diff --git a/src/main/java/com/eactive/testmaster/user/service/StaffUserService.java b/src/main/java/com/eactive/testmaster/user/service/StaffUserService.java index a12ba37..be88d0a 100644 --- a/src/main/java/com/eactive/testmaster/user/service/StaffUserService.java +++ b/src/main/java/com/eactive/testmaster/user/service/StaffUserService.java @@ -62,7 +62,15 @@ public class StaffUserService { staffUserRepository.save(user); - - } + + public void updatePassword(String id, String newPassword) { + StaffUser user = staffUserRepository.findById(id).orElseThrow(() -> new UserNotFoundException(USER_NOT_FOUND_MESSAGE)); + user.setUserSecurity(newPassword); + user.setLockAt(EnabledStatus.N); + user.setLockCnt(0); + user.setLastLockDate(null); + staffUserRepository.save(user); + } + } diff --git a/src/main/java/com/eactive/testmaster/user/service/UserService.java b/src/main/java/com/eactive/testmaster/user/service/UserService.java index e910b81..9b092d6 100644 --- a/src/main/java/com/eactive/testmaster/user/service/UserService.java +++ b/src/main/java/com/eactive/testmaster/user/service/UserService.java @@ -3,10 +3,17 @@ package com.eactive.testmaster.user.service; import com.eactive.testmaster.common.entity.EnabledStatus; import com.eactive.testmaster.common.exception.UserNotFoundException; import com.eactive.testmaster.common.util.EncryptionUtil; +import com.eactive.testmaster.login.entity.Token; +import com.eactive.testmaster.login.service.TokenService; import com.eactive.testmaster.user.entity.StaffUser; import com.eactive.testmaster.user.entity.User; import com.eactive.testmaster.user.repository.StaffUserRepository; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -17,14 +24,23 @@ import org.springframework.stereotype.Service; //@Transactional(propagation = Propagation.NESTED) public class UserService implements UserDetailsService { + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + public static final String USER_NOT_FOUND_MESSAGE = "사용자 정보가 존재하지 않습니다."; - @Autowired - StaffUserRepository staffUserRepository; + private final StaffUserRepository staffUserRepository; - @Autowired private EncryptionUtil encryptionUtil; + private final TokenService tokenService; + + public UserService(StaffUserRepository staffUserRepository, EncryptionUtil encryptionUtil, TokenService tokenService) { + this.staffUserRepository = staffUserRepository; + this.encryptionUtil = encryptionUtil; + this.tokenService = tokenService; + } + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { try { @@ -65,6 +81,25 @@ public class UserService implements UserDetailsService { staffUserRepository.save((StaffUser) user); } + public void requestPasswordReset(String email) { + + Token token = tokenService.createToken(email); + Map params = new HashMap<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + String formattedDateTime = token.getExpiresAt().format(formatter); + + params.put("userId", email); + params.put("token", token.getTokenValue()); + params.put("expiresAt", formattedDateTime); + + logger.info("requestPasswordReset::params= {}", params); + } + + public void resetPassword(String email, String newPassword) { + User user = findByUserId(email); + updatePassword(user, newPassword); + } + public void updateUser(User user) { staffUserRepository.save((StaffUser) user); } diff --git a/src/main/resources/templates/page/forgotPassword.html b/src/main/resources/templates/page/forgotPassword.html new file mode 100644 index 0000000..e2afa1f --- /dev/null +++ b/src/main/resources/templates/page/forgotPassword.html @@ -0,0 +1,86 @@ + + + + + + +
+ +
+
+

비밀번호 초기화

+
+ +
+
+
+
+ +
+ + +
+
+ +
+

비밀번호 초기화 링크가 만료되었습니다.

+
+
+

+
+
+ +
+
+
+
+
+ + +
+
+ +
+
+
+
+ 로그인 +
+
+ +
+ + + + + + + + + diff --git a/src/main/resources/templates/page/resetPassword.html b/src/main/resources/templates/page/resetPassword.html new file mode 100644 index 0000000..a89c4f1 --- /dev/null +++ b/src/main/resources/templates/page/resetPassword.html @@ -0,0 +1,80 @@ + + + + + + +
+ +
+
+

비밀번호 초기화

+
+ +
+
+
+
+ +
+ + + +
+
+
+ +
+ + + +
+
+
+
+
+

비밀번호 초기화 되었습니다.

+
+
+ +
+
+
+ 로그인 +
+
+ +
+ + + + + + + + + diff --git a/src/main/resources/templates/page/users/staffPasswordReset.html b/src/main/resources/templates/page/users/staffPasswordReset.html new file mode 100644 index 0000000..e174204 --- /dev/null +++ b/src/main/resources/templates/page/users/staffPasswordReset.html @@ -0,0 +1,90 @@ + + + + + + +
+
+
+
+

사용자 비밀 번호 변경

+
+
+
+ + +
+ + +
+ +
+ + + +
+
+
+ +
+ + + +
+
+
+
+
+ + +
+
+
+ + + + + + + + + + diff --git a/src/main/resources/templates/page/users/staffUserList.html b/src/main/resources/templates/page/users/staffUserList.html index 418fe32..bbd70ce 100644 --- a/src/main/resources/templates/page/users/staffUserList.html +++ b/src/main/resources/templates/page/users/staffUserList.html @@ -6,106 +6,109 @@
-
-
-
-

사용자 목록

+
+
+
+

사용자 목록

+
+
+
+
+ +
+
-
-
- - -
- -
-
- -
-
- -
-
- -
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
번호
-
- -
-
ID
이름
상태
비고
-
- - -
-
- - -
- -
-
-
- -
-
-
- -
-
-
-
- +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
번호
+
+ +
+
ID
이름
상태
비고
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+
+ + +
+
+
+
+ +
+
@@ -113,59 +116,67 @@ - + fnAdd.addEventListener("click", function () { + location.href = '[[@{/mgmt/users/create_view.do}]]'; + }); + }) +