diff --git a/src/main/java/alfio/controller/api/admin/UsersApiController.java b/src/main/java/alfio/controller/api/admin/UsersApiController.java index d9b6822f91..e05cfa4c67 100644 --- a/src/main/java/alfio/controller/api/admin/UsersApiController.java +++ b/src/main/java/alfio/controller/api/admin/UsersApiController.java @@ -27,6 +27,8 @@ import alfio.util.ImageUtil; import alfio.util.Json; import alfio.util.ValidationResult; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; @@ -137,6 +139,12 @@ public String editUser(@RequestBody UserModification userModification, Principal return OK; } + @RequestMapping(value = "/users/update-password", method = POST) + public ValidationResult updatePassword(@RequestBody PasswordModification passwordModification, Principal principal) { + return userManager.validateNewPassword(principal.getName(), passwordModification.oldPassword, passwordModification.newPassword, passwordModification.newPasswordConfirm) + .ifSuccess(() -> userManager.updatePassword(principal.getName(), passwordModification.newPassword)); + } + @RequestMapping(value = "/users/new", method = POST) public UserWithPassword insertUser(@RequestBody UserModification userModification, HttpSession session, Principal principal) { Role requested = Role.valueOf(userModification.getRole()); @@ -179,6 +187,13 @@ public UserModification loadUser(@PathVariable("id") int userId) { return new UserModification(user.getId(), userOrganizations.get(0).getId(), userManager.getUserRole(user).name(), user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmailAddress()); } + @RequestMapping(value = "/users/current", method = GET) + public UserModification loadCurrentUser(Principal principal) { + User user = userManager.findUserByUsername(principal.getName()); + List userOrganizations = userManager.findUserOrganizations(user); + return new UserModification(user.getId(), userOrganizations.get(0).getId(), userManager.getUserRole(user).name(), user.getUsername(), user.getFirstName(), user.getLastName(), user.getEmailAddress()); + } + @RequestMapping(value = "/users/{id}/reset-password", method = PUT) public UserWithPassword resetPassword(@PathVariable("id") int userId, HttpSession session) { UserWithPassword userWithPassword = userManager.resetPassword(userId); @@ -190,7 +205,7 @@ private void storePasswordImage(HttpSession session, UserWithPassword userWithPa session.setAttribute(USER_WITH_PASSWORD_KEY, userWithPassword); } - static final class RoleDescriptor { + private static final class RoleDescriptor { private final Role role; RoleDescriptor(Role role) { @@ -205,4 +220,20 @@ public String getDescription() { return role.getDescription(); } } + + private static final class PasswordModification { + + private final String oldPassword; + private final String newPassword; + private final String newPasswordConfirm; + + @JsonCreator + private PasswordModification(@JsonProperty("oldPassword") String oldPassword, + @JsonProperty("newPassword") String newPassword, + @JsonProperty("newPasswordConfirm") String newPasswordConfirm) { + this.oldPassword = oldPassword; + this.newPassword = newPassword; + this.newPasswordConfirm = newPasswordConfirm; + } + } } diff --git a/src/main/java/alfio/manager/user/UserManager.java b/src/main/java/alfio/manager/user/UserManager.java index 29c800b4c4..587a32c0dd 100644 --- a/src/main/java/alfio/manager/user/UserManager.java +++ b/src/main/java/alfio/manager/user/UserManager.java @@ -169,13 +169,18 @@ public ValidationResult validateOrganization(Integer id, String name, String ema @Transactional public void editUser(int id, int organizationId, String username, String firstName, String lastName, String emailAddress, Role role, String currentUsername) { - int userOrganizationResult = userOrganizationRepository.updateUserOrganization(id, organizationId); - Assert.isTrue(userOrganizationResult == 1, "unexpected error during organization update"); + boolean admin = ADMIN_USERNAME.equals(username) && Role.ADMIN == role; + if(!admin) { + int userOrganizationResult = userOrganizationRepository.updateUserOrganization(id, organizationId); + Assert.isTrue(userOrganizationResult == 1, "unexpected error during organization update"); + } int userResult = userRepository.update(id, username, firstName, lastName, emailAddress); Assert.isTrue(userResult == 1, "unexpected error during user update"); - Assert.isTrue(getAvailableRoles(currentUsername).contains(role), "cannot assign role "+role); - authorityRepository.revokeAll(username); - authorityRepository.create(username, role.getRoleName()); + if(!admin) { + Assert.isTrue(getAvailableRoles(currentUsername).contains(role), "cannot assign role "+role); + authorityRepository.revokeAll(username); + authorityRepository.create(username, role.getRoleName()); + } } @Transactional @@ -196,6 +201,14 @@ public UserWithPassword resetPassword(int userId) { return new UserWithPassword(user, password, UUID.randomUUID().toString()); } + @Transactional + public boolean updatePassword(String username, String newPassword) { + User user = userRepository.findByUsername(username).stream().findFirst().orElseThrow(IllegalStateException::new); + Validate.isTrue(PasswordGenerator.isValid(newPassword), "invalid password"); + //Validate.isTrue(userRepository.resetPassword(user.getId(), passwordEncoder.encode(newPassword)) == 1, "error during password update"); + return true; + } + @Transactional public void deleteUser(int userId, String currentUsername) { User currentUser = userRepository.findEnabledByUsername(currentUsername).orElseThrow(IllegalArgumentException::new); @@ -219,5 +232,26 @@ public ValidationResult validateUser(Integer id, String username, int organizati .collect(toList())); } + public ValidationResult validateNewPassword(String username, String oldPassword, String newPassword, String newPasswordConfirm) { + return userRepository.findByUsername(username) + .stream() + .findFirst() + .map(u -> { + List errors = new ArrayList<>(); + Optional password = userRepository.findPasswordByUsername(username); + if(!password.filter(p -> passwordEncoder.matches(oldPassword, p)).isPresent()) { + errors.add(new ValidationResult.ValidationError("old-password-invalid", "wrong password")); + } + if(!PasswordGenerator.isValid(newPassword)) { + errors.add(new ValidationResult.ValidationError("new-password-invalid", "new password is not strong enough")); + } + if(!StringUtils.equals(newPassword, newPasswordConfirm)) { + errors.add(new ValidationResult.ValidationError("new-password-does-not-match", "new password has not been confirmed")); + } + return ValidationResult.of(errors); + }) + .orElseGet(ValidationResult::failed); + } + } diff --git a/src/main/java/alfio/repository/user/UserRepository.java b/src/main/java/alfio/repository/user/UserRepository.java index 88f37d5438..d3929e7117 100644 --- a/src/main/java/alfio/repository/user/UserRepository.java +++ b/src/main/java/alfio/repository/user/UserRepository.java @@ -37,6 +37,9 @@ public interface UserRepository { @Query("select * from ba_user where username = :username and enabled = true") Optional findEnabledByUsername(@Bind("username") String username); + @Query("select password from ba_user where username = :username and enabled = true") + Optional findPasswordByUsername(@Bind("username") String username); + @Query("INSERT INTO ba_user(username, password, first_name, last_name, email_address, enabled) VALUES" + " (:username, :password, :first_name, :last_name, :email_address, :enabled)") @AutoGeneratedKey("id") diff --git a/src/main/java/alfio/util/PasswordGenerator.java b/src/main/java/alfio/util/PasswordGenerator.java index baa8a84053..a61bab9f0d 100644 --- a/src/main/java/alfio/util/PasswordGenerator.java +++ b/src/main/java/alfio/util/PasswordGenerator.java @@ -23,6 +23,7 @@ import java.util.*; import java.util.function.IntConsumer; +import java.util.regex.Pattern; import java.util.stream.IntStream; public final class PasswordGenerator { @@ -31,6 +32,7 @@ public final class PasswordGenerator { private static final boolean DEV_MODE; private static final int MAX_LENGTH = 14; private static final int MIN_LENGTH = 10; + private static final Pattern VALIDATION_PATTERN; static { List chars = new LinkedList<>(); @@ -56,6 +58,7 @@ public final class PasswordGenerator { DEV_MODE = Arrays.stream(Optional.ofNullable(System.getProperty("spring.profiles.active")).map(p -> p.split(",")).orElse(new String[0])) .map(StringUtils::trim) .anyMatch(Initializer.PROFILE_DEV::equals); + VALIDATION_PATTERN = Pattern.compile("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*\\p{Punct})(?=\\S+$).{"+MIN_LENGTH+",}$");//source: http://stackoverflow.com/a/3802238 } private PasswordGenerator() { @@ -69,4 +72,8 @@ public static String generateRandomPassword() { int length = MIN_LENGTH + r.nextInt(MAX_LENGTH - MIN_LENGTH + 1); return RandomStringUtils.random(length, PASSWORD_CHARACTERS); } + + public static boolean isValid(String password) { + return StringUtils.isNotBlank(password) && VALIDATION_PATTERN.matcher(password).matches(); + } } diff --git a/src/main/webapp/WEB-INF/templates/admin/index.ms b/src/main/webapp/WEB-INF/templates/admin/index.ms index fd578e8af0..a7e50c6dbb 100644 --- a/src/main/webapp/WEB-INF/templates/admin/index.ms +++ b/src/main/webapp/WEB-INF/templates/admin/index.ms @@ -56,13 +56,14 @@
  • Dashboard
  • Configuration
  • -
  • {{username}}
  • +
  • {{username}}
  • +
  • Change Password
  • Logout
  • diff --git a/src/main/webapp/resources/angular-templates/admin/partials/main/edit-current-user.html b/src/main/webapp/resources/angular-templates/admin/partials/main/edit-current-user.html new file mode 100644 index 0000000000..9718e299c9 --- /dev/null +++ b/src/main/webapp/resources/angular-templates/admin/partials/main/edit-current-user.html @@ -0,0 +1,73 @@ +
    + +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + +
    +
    + loading... +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + loading... +
    +
    + + +
    + +
    +
    + The new password doesn't match the required format. It must be at least 8 characters and it must: +
      +
    • contain at least an uppercase letter A-Z
    • +
    • contain at least a lowercase letter a-z
    • +
    • contain at least a punctuation character !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~}
    • +
    • not contain spaces
    • +
    +
    +
    "New password" and "Confirm new password" don't match
    +
    Current password is not valid
    +
    The value should match the following pattern: {{requiredPattern}}
    +
    + +
    + +
    \ No newline at end of file diff --git a/src/main/webapp/resources/js/admin/feature/user/user.js b/src/main/webapp/resources/js/admin/feature/user/user.js index 49199f7de9..0940a90ba0 100644 --- a/src/main/webapp/resources/js/admin/feature/user/user.js +++ b/src/main/webapp/resources/js/admin/feature/user/user.js @@ -43,9 +43,23 @@ } } }) + .state('edit-current-user', { + url: "/profile/edit", + templateUrl: "/resources/angular-templates/admin/partials/main/edit-current-user.html", + controller: 'EditCurrentUserController', + controllerAs: 'editCurrentUserCtrl', + resolve: { + user: ['UserService', function (UserService) { + return UserService.loadCurrentUser().then(function(resp) { + return resp.data; + }); + }] + } + }) }]) .controller('EditOrganizationController', EditOrganizationController) .controller('EditUserController', EditUserController) + .controller('EditCurrentUserController', EditCurrentUserController) .controller('OrganizationListController', OrganizationListController) .controller('UsersListController', UsersListController) .service('OrganizationService', OrganizationService) @@ -133,6 +147,56 @@ EditUserController.$inject = ['$state', '$stateParams', '$rootScope', '$q', 'OrganizationService', 'UserService', 'ValidationService', '$q']; + function EditCurrentUserController($state, user, UserService, ValidationService) { + var self = this; + self.user = user; + self.original = user; + + self.saveUserInfo = function() { + if(self.editUser.$valid) { + self.loading = true; + var promise = UserService.checkUser(self.user).then(function() { + return UserService.editUser(self.user).then(function() { + self.original = angular.copy(self.user); + self.loading = false; + }); + }); + promise.then(function() {}, function(err) { + self.loading = false; + self.error = true; + }); + } + }; + + self.doReset = function() { + self.user = angular.copy(self.original); + self.passwordContainer = {}; + self.changePasswordErrors = {}; + }; + + self.updatePassword = function() { + if(self.changePassword.$valid) { + self.loading = true; + UserService.updatePassword(self.passwordContainer).then(function(result) { + var validationResult = result.data; + if(validationResult.success) { + self.passwordContainer = {}; + self.changePasswordErrors = {}; + alert('succeeded'); + } else { + angular.forEach(validationResult.validationErrors, function(e) { + self.changePassword.$setValidity(e.fieldName, false); + }); + } + self.loading = false; + }); + } + }; + + } + + EditCurrentUserController.$inject = ['$state', 'user', 'UserService', 'ValidationService']; + function OrganizationService($http, HttpErrorHandler) { return { getAllOrganizations : function() { @@ -173,6 +237,12 @@ loadUser: function(userId) { return $http.get('/admin/api/users/'+userId+'.json').error(HttpErrorHandler.handle); }, + loadCurrentUser: function() { + return $http.get('/admin/api/users/current.json').error(HttpErrorHandler.handle); + }, + updatePassword: function(passwordContainer) { + return $http.post('/admin/api/users/update-password.json', passwordContainer).error(HttpErrorHandler.handle); + }, deleteUser: function(user) { return $http['delete']('/admin/api/users/'+user.id).error(HttpErrorHandler.handle); },