Merge branch '89-restore-password' into 'dev'

Resolve "Восстановление пароля"

Closes #89

See merge request romanov73/ng-tracker!101
environments/staging/deployments/68
Anton Romanov 5 years ago
commit d4364b9767

@ -21,4 +21,5 @@ public class Constants {
public static final String PASSWORD_RESET_REQUEST_PAGE = "/resetRequest"; public static final String PASSWORD_RESET_REQUEST_PAGE = "/resetRequest";
public static final String PASSWORD_RESET_PAGE = "/reset"; public static final String PASSWORD_RESET_PAGE = "/reset";
public static final int RESET_KEY_LENGTH = 6;
} }

@ -104,7 +104,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
.antMatchers("/css/**") .antMatchers("/css/**")
.antMatchers("/js/**") .antMatchers("/js/**")
.antMatchers("/templates/**") .antMatchers("/templates/**")
.antMatchers("/webjars/**"); .antMatchers("/webjars/**")
.antMatchers("/img/**");
} }
@Autowired @Autowired

@ -35,11 +35,6 @@ public class AdviceController {
this.userService = userService; this.userService = userService;
} }
@ModelAttribute("currentUser")
public String getCurrentUser() {
return userService.getCurrentUser().getUserAbbreviate();
}
@ModelAttribute("flashMessage") @ModelAttribute("flashMessage")
public String getFlashMessage() { public String getFlashMessage() {
return null; return null;

@ -9,9 +9,9 @@ public enum ErrorConstants {
USER_EMAIL_EXISTS(102, "Пользователь с таким почтовым ящиком уже существует"), USER_EMAIL_EXISTS(102, "Пользователь с таким почтовым ящиком уже существует"),
USER_LOGIN_EXISTS(103, "Пользователь с таким логином уже существует"), USER_LOGIN_EXISTS(103, "Пользователь с таким логином уже существует"),
USER_PASSWORDS_NOT_VALID_OR_NOT_MATCH(104, "Пароли введены неверно"), USER_PASSWORDS_NOT_VALID_OR_NOT_MATCH(104, "Пароли введены неверно"),
USER_NOT_FOUND(105, "User is not found"), USER_NOT_FOUND(105, "Аккаунт не найден"),
USER_NOT_ACTIVATED(106, "User is not activated"), USER_NOT_ACTIVATED(106, "User is not activated"),
USER_RESET_ERROR(107, "Invalid reset key"), USER_RESET_ERROR(107, "Некорректный ключ подтверждения"),
USER_UNDEAD_ERROR(108, "Can't edit/delete that user"), USER_UNDEAD_ERROR(108, "Can't edit/delete that user"),
FILE_UPLOAD_ERROR(110, "File upload error"), FILE_UPLOAD_ERROR(110, "File upload error"),
USER_SENDING_MAIL_EXCEPTION(111, "Во время отправки приглашения пользователю произошла ошибка"); USER_SENDING_MAIL_EXCEPTION(111, "Во время отправки приглашения пользователю произошла ошибка");

@ -148,16 +148,15 @@ public class UserController extends OdinController<UserListDto, UserDto> {
} }
@PostMapping(PASSWORD_RESET_REQUEST_URL) @PostMapping(PASSWORD_RESET_REQUEST_URL)
public Response<Boolean> requestPasswordReset(@RequestParam("email") String email) { public void requestPasswordReset(@RequestParam("email") String email) {
log.debug("REST: UserController.requestPasswordReset( {} )", email); log.debug("REST: UserController.requestPasswordReset( {} )", email);
return new Response<>(userService.requestUserPasswordReset(email)); userService.requestUserPasswordReset(email);
} }
@PostMapping(PASSWORD_RESET_URL) @PostMapping(PASSWORD_RESET_URL)
public Response<Boolean> finishPasswordReset(@RequestParam("key") String key, public Response<Boolean> finishPasswordReset(@RequestBody UserResetPasswordDto userResetPasswordDto) {
@RequestBody UserResetPasswordDto userResetPasswordDto) { log.debug("REST: UserController.requestPasswordReset( {} )", userResetPasswordDto.getResetKey());
log.debug("REST: UserController.requestPasswordReset( {} )", key); return new Response<>(userService.completeUserPasswordReset(userResetPasswordDto));
return new Response<>(userService.completeUserPasswordReset(key, userResetPasswordDto));
} }
@PostMapping("/changePassword") @PostMapping("/changePassword")

@ -14,6 +14,10 @@ public class UserResetPasswordDto {
@Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50)
private String passwordConfirm; private String passwordConfirm;
@NotEmpty
@Size(min = Constants.RESET_KEY_LENGTH)
private String resetKey;
public String getPassword() { public String getPassword() {
return password; return password;
} }
@ -25,4 +29,8 @@ public class UserResetPasswordDto {
public boolean isPasswordsValid() { public boolean isPasswordsValid() {
return Objects.equals(password, passwordConfirm); return Objects.equals(password, passwordConfirm);
} }
public String getResetKey() {
return resetKey;
}
} }

@ -106,8 +106,7 @@ public class MailService {
sendEmailFromTemplate(user, "activationEmail", Constants.MAIL_ACTIVATE); sendEmailFromTemplate(user, "activationEmail", Constants.MAIL_ACTIVATE);
} }
@Async public void sendPasswordResetMail(User user) throws MessagingException, MailException {
public void sendPasswordResetMail(User user) {
sendEmailFromTemplate(user, "passwordResetEmail", Constants.MAIL_RESET); sendEmailFromTemplate(user, "passwordResetEmail", Constants.MAIL_RESET);
} }
@ -118,6 +117,5 @@ public class MailService {
@Async @Async
public void sendChangePasswordMail(User user) { public void sendChangePasswordMail(User user) {
sendEmailFromTemplate(user, "passwordChangeEmail", Constants.MAIL_CHANGE_PASSWORD); sendEmailFromTemplate(user, "passwordChangeEmail", Constants.MAIL_CHANGE_PASSWORD);
} }
} }

@ -236,7 +236,7 @@ public class UserService implements UserDetailsService {
mailService.sendChangePasswordMail(user); mailService.sendChangePasswordMail(user);
} }
public boolean requestUserPasswordReset(String email) { public boolean requestUserPasswordReset(String email) {
User user = userRepository.findOneByEmailIgnoreCase(email); User user = userRepository.findOneByEmailIgnoreCase(email);
if (user == null) { if (user == null) {
throw new UserNotFoundException(email); throw new UserNotFoundException(email);
@ -247,23 +247,30 @@ public class UserService implements UserDetailsService {
user.setResetKey(UserUtils.generateResetKey()); user.setResetKey(UserUtils.generateResetKey());
user.setResetDate(new Date()); user.setResetDate(new Date());
user = userRepository.save(user); user = userRepository.save(user);
mailService.sendPasswordResetMail(user); try {
mailService.sendPasswordResetMail(user);
} catch (MessagingException | MailException e) {
throw new UserSendingMailException(email);
}
log.debug("Created Reset Password Request for User: {}", user.getLogin()); log.debug("Created Reset Password Request for User: {}", user.getLogin());
return true; return true;
} }
public boolean completeUserPasswordReset(String key, UserResetPasswordDto userResetPasswordDto) { public boolean completeUserPasswordReset(UserResetPasswordDto userResetPasswordDto) {
if (!userResetPasswordDto.isPasswordsValid()) { if (!userResetPasswordDto.isPasswordsValid()) {
throw new UserPasswordsNotValidOrNotMatchException(""); throw new UserPasswordsNotValidOrNotMatchException("Пароли не совпадают");
} }
User user = userRepository.findOneByResetKey(key); User user = userRepository.findOneByResetKey(userResetPasswordDto.getResetKey());
if (user == null) { if (user == null) {
throw new UserResetKeyError(key); throw new UserResetKeyError(userResetPasswordDto.getResetKey());
} }
user.setPassword(passwordEncoder.encode(userResetPasswordDto.getPassword())); user.setPassword(passwordEncoder.encode(userResetPasswordDto.getPassword()));
user.setResetKey(null); user.setResetKey(null);
user.setResetDate(null); user.setResetDate(null);
user = userRepository.save(user); user = userRepository.save(user);
mailService.sendChangePasswordMail(user);
log.debug("Reset Password for User: {}", user.getLogin()); log.debug("Reset Password for User: {}", user.getLogin());
return true; return true;
} }

@ -1,23 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <head>
<title>Password reset</title> <title>Восстановление пароля</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="shortcut icon" th:href="@{|${baseUrl}/favicon.ico|}"/> <link rel="shortcut icon" th:href="@{|${baseUrl}/favicon.ico|}"/>
</head> </head>
<body> <body>
<p> <p>
Dear <span th:text="${user.firstName + ' ' + user.lastName}">Ivan Ivanov</span> Дорогой <span th:text="${user.firstName + ' ' + user.lastName}">Ivan Ivanov</span>
</p> </p>
<p> <p>
For your account a password reset was requested, please click on the URL below to Ваш ключ для восстановления пароля <span th:text="${user.resetKey}"></span>
</p> </p>
<p> <p>
<a th:href="@{|${baseUrl}/reset?key=${user.resetKey}|}" С уважением,
th:text="@{|${baseUrl}/reset?key=${user.resetKey}|}">Reset Link</a>
</p>
<p>
Regards,
<br/> <br/>
<em>Balance Team.</em> <em>Balance Team.</em>
</p> </p>

@ -0,0 +1,3 @@
.loader {
padding-left:50%
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

@ -1,7 +1,7 @@
function changePassword() { function changePassword() {
oldPassword = document.getElementById("oldPassword").value oldPassword = $("#oldPassword").val()
password = document.getElementById("password").value password = $("#password").val()
confirmPassword = document.getElementById("confirmPassword").value confirmPassword = $("#confirmPassword").val()
if ([oldPassword.length, password.length, confirmPassword.length].includes(0)) { if ([oldPassword.length, password.length, confirmPassword.length].includes(0)) {
showFeedbackMessage("Заполните все поля", MessageTypesEnum.WARNING); showFeedbackMessage("Заполните все поля", MessageTypesEnum.WARNING);
@ -23,7 +23,7 @@ function changePassword() {
}), }),
method: "POST", method: "POST",
success: function() { success: function() {
document.getElementById("closeModalPassword").click(); $("#closeModalPassword").click();
showFeedbackMessage("Пароль был обновлен", MessageTypesEnum.SUCCESS) showFeedbackMessage("Пароль был обновлен", MessageTypesEnum.SUCCESS)
}, },
@ -34,25 +34,94 @@ function changePassword() {
} }
function inviteUser() { function inviteUser() {
email = document.getElementById("email").value; email = $("#email").val();
re = /\S+@\S+\.\S+/; if (!isEmailValid(email)) {
showFeedbackMessage("Некорректный почтовый ящик", MessageTypesEnum.WARNING);
return;
}
$.ajax({
url:"/api/1.0/users/invite?email=" + email,
contentType: "application/json; charset=utf-8",
method: "POST",
success: function() {
$("#closeModalInvite").click();
showFeedbackMessage("Пользователь был успешно приглашен", MessageTypesEnum.SUCCESS)
},
error: function(errorData) {
showFeedbackMessage(errorData.responseJSON.error.message, MessageTypesEnum.WARNING)
}
})
}
function requestResetPassword() {
email = $("#emailReset").val()
if (!re.test(email)) { if (!isEmailValid(email)) {
showFeedbackMessage("Некорректный почтовый ящик", MessageTypesEnum.WARNING); showFeedbackMessage("Некорректный почтовый ящик", MessageTypesEnum.WARNING);
return; return;
} }
$("#dvloader").show();
$.ajax({ $.ajax({
url:"/api/1.0/users/invite?email=" + email, url:"/api/1.0/users/password-reset-request?email=" + email,
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
method: "POST", method: "POST",
success: function() { success: function() {
document.getElementById("closeModalInvite").click(); showFeedbackMessage("Проверочный код был отправлен на указанный почтовый ящик", MessageTypesEnum.SUCCESS)
$("#passwordNew").show()
$("#passwordConfirm").show()
$("#btnReset").show()
$("#resetKey").show()
$("#emailReset").hide()
$("#btnSend").hide()
$("#dvloader").hide()
},
error: function(errorData) {
showFeedbackMessage(errorData.responseJSON.error.message, MessageTypesEnum.WARNING)
$("#dvloader").hide()
}
})
}
function resetPassword() {
passwordNew = $("#passwordNew").val();
passwordConfirm = $("#passwordConfirm").val();
resetKey = $("#resetKey").val();
if ([passwordNew, passwordConfirm, resetKey].includes("")) {
showFeedbackMessage("Заполните все поля", MessageTypesEnum.WARNING);
return;
}
if (passwordNew != passwordConfirm) {
showFeedbackMessage("Пароли не совпадают", MessageTypesEnum.WARNING);
return;
}
$.ajax({
url:"/api/1.0/users/password-reset",
contentType: "application/json; charset=utf-8",
method: "POST",
data: JSON.stringify({
"password": passwordNew,
"passwordConfirm": passwordConfirm,
"resetKey": resetKey,
}),
success: function() {
showFeedbackMessage("Пользователь был успешно приглашен", MessageTypesEnum.SUCCESS) showFeedbackMessage("Пользователь был успешно приглашен", MessageTypesEnum.SUCCESS)
window.location.href = "/login"
}, },
error: function(errorData) { error: function(errorData) {
showFeedbackMessage(errorData.responseJSON.error.message, MessageTypesEnum.WARNING) showFeedbackMessage(errorData.responseJSON.error.message, MessageTypesEnum.WARNING)
} }
}) })
}
function isEmailValid(email) {
re = /\S+@\S+\.\S+/;
return re.test(email)
} }

@ -1,57 +1,59 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" <html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="default"> layout:decorator="default" xmlns:th="http://www.w3.org/1999/xhtml">
<head> <head>
<script src="/js/users.js"></script>
<link rel="stylesheet" href="../css/base.css"/>
</head> </head>
<body> <body>
<nav layout:fragment="navbar"> <nav layout:fragment="navbar">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="/"><span class="ui-menuitem-text"><i <a class="navbar-brand" href="/"><span class="ui-menuitem-text"><i
class="fa fa-plane fa-4" aria-hidden="true"></i> Balance</span></a> class="fa fa-plane fa-4" aria-style="display:none"></i> Balance</span></a>
</div> </div>
</nav> </nav>
<div class="container" layout:fragment="content"> <div layout:fragment="content">
<form id="reset-form" method="post" class="margined-top-10"> <section class="bg-light" id="portfolio">
<fieldset>
<div class="form-group"> <div class="container">
<input type="email" name="email" id="email" class="form-control" <div class="row justify-content-md-center">
placeholder="E-Mail" required="true" autofocus="autofocus"/> <div class="col-lg-6">
</div> <div class="form-group">
<button type="submit" class="btn btn-success btn-block">Сбросить пароль</button> <input type="text" name="email" id="emailReset" class="form-control"
<div class="form-group"> placeholder="E-Mail"/>
<small class="form-text text-muted"> </div>
<a href="/login">Вернуться к странице входа</a> <div class="form-group">
</small> <input type="password" name="email" id="passwordNew" class="form-control"
placeholder="Новый пароль" style="display:none"/>
</div>
<div class="form-group">
<input type="password" name="email" id="passwordConfirm" class="form-control"
placeholder="Подтвердите пароль" style="display:none"/>
</div>
<div class="form-group">
<input type="text" name="email" id="resetKey" class="form-control"
placeholder="Код подтверждения" style="display:none"/>
</div>
<div id="dvloader" class="loader" style="display:none"><img src="../img/main/ajax-loader.gif" /></div>
<button id="btnSend" type="button" onclick="requestResetPassword()"
class="btn btn-success btn-block">
Отправить код подтверждения
</button>
<button id="btnReset" style="display:none" type="button" onclick="resetPassword()"
class="btn btn-success btn-block">
Сбросить
пароль
</button>
<div class="form-group">
<small class="form-text text-muted">
<a href="/login">Вернуться к странице входа</a>
</small>
</div>
</div>
</div> </div>
</fieldset> </div>
</form> </section>
</div> </div>
<th:block layout:fragment="data-scripts">
<script type="text/javascript">
/*<![CDATA[*/
$(document).ready(function () {
$("#reset-form").submit(function () {
var email = $("#email").val();
if (isEmpty(email)) {
showFeedbackMessage("Адрес электронной почты не задан", MessageTypesEnum.DANGER);
return false;
}
postToRest(urlUsersPasswordResetRequest + "?email=" + email, null,
function () {
showFeedbackMessage("Запрос на смену пароля отправлен");
},
function () {
$("#email").val("");
}
);
return false;
});
});
/*]]>*/
</script>
</th:block>
</body> </body>
</html> </html>
Loading…
Cancel
Save