Revision: 4b9ea5e0d960 Author: Andrew Lindesay <apl@xxxxxxxxxxxxxx> Date: Thu Jan 16 08:37:44 2014 UTC Log: + implemented a change password function+ implemented basic validation to check the complexity of a supplied password
http://code.google.com/p/haiku-depot-web-app/source/detail?r=4b9ea5e0d960 Added:/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/ChangePasswordRequest.java /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/ChangePasswordResult.java /haikudepotserver-webapp/src/main/webapp/js/app/controller/changepassword.html /haikudepotserver-webapp/src/main/webapp/js/app/controller/changepasswordcontroller.js /haikudepotserver-webapp/src/main/webapp/js/app/directive/userlabeldirective.js /haikudepotserver-webapp/src/main/webapp/js/app/directive/validpassworddirective.js /haikudepotserver-webapp/src/main/webapp/js/app/service/breadcrumbsservice.js
Modified:/haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthenticationService.java
/haikudepotserver-webapp/src/main/resources/messages.properties/haikudepotserver-webapp/src/main/resources/spring/webresourcegroup-context.xml
/haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css/haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateuser.html /haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateusercontroller.js
/haikudepotserver-webapp/src/main/webapp/js/app/controller/createuser.html/haikudepotserver-webapp/src/main/webapp/js/app/controller/createusercontroller.js
/haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgicon.html /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewuser.html/haikudepotserver-webapp/src/main/webapp/js/app/controller/viewusercontroller.js
/haikudepotserver-webapp/src/main/webapp/js/app/routes.js ======================================= --- /dev/null+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/ChangePasswordRequest.java Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,21 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +/**+ * <p>This is the request model object for changing a password. For details on the captcha token, see the documentation + * supplied on {@link org.haikuos.haikudepotserver.api1.model.user.CreateUserRequest}.</p>
+ */ + +public class ChangePasswordRequest { + + public String nickname; + public String oldPasswordClear; + public String newPasswordClear; + public String captchaToken; + public String captchaResponse; + +} ======================================= --- /dev/null+++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/model/user/ChangePasswordResult.java Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,9 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +package org.haikuos.haikudepotserver.api1.model.user; + +public class ChangePasswordResult { +} ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/changepassword.html Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,78 @@ +<breadcrumbs items="breadcrumbItems"></breadcrumbs> + +<div class="content-container"> + <form name="changePasswordForm" novalidate="novalidate"> + + <label>Nickname</label> + <div class="form-control-group"> + <div class="form-control-group-static"> + <user-label user="user"></user-label> + </div> + </div> + + <label for="old-password-clear">Existing Password</label>+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('oldPasswordClear')">
+ <input + id="old-password-clear" + type="password" + name="oldPasswordClear" + ng-change="oldPasswordChanged()" + ng-required="true" + ng-model="changePasswordData.oldPasswordClear"></input>+ <error-messages key-prefix="changePassword.oldPasswordClear" error="changePasswordForm.oldPasswordClear.$error"></error-messages>
+ </div> + + <label for="new-password-clear">New Password</label>+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('newPasswordClear')">
+ <input + id="new-password-clear" + type="password" + name="newPasswordClear" + valid-password="" + ng-change="newPasswordsChanged()" + ng-required="true" + ng-model="changePasswordData.newPasswordClear"></input>+ <error-messages key-prefix="changePassword.newPasswordClear" error="changePasswordForm.newPasswordClear.$error"></error-messages>
+ </div> ++ <label for="new-password-clear-repeated">New Password Repeated</label> + <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('newPasswordClearRepeated')">
+ <input + id="new-password-clear-repeated" + type="password" + name="newPasswordClearRepeated" + ng-change="newPasswordsChanged()" + ng-required="true"+ ng-model="changePasswordData.newPasswordClearRepeated"></input> + <error-messages key-prefix="changePassword.newPasswordClearRepeated" error="changePasswordForm.newPasswordClearRepeated.$error"></error-messages>
+ </div> + + <label for="captcha-response-input">Check for a Person</label>+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('captchaResponse')"> + <img style="vertical-align:middle;" src="{{captchaImageUrl}}"></img>
+ = + <input + id="captcha-response-input" + size="3" + type="text" + name="captchaResponse" + ng-required="true" + ng-change="captchaResponseDidChange()" + ng-model="changePasswordData.captchaResponse"></input>+ <error-messages key-prefix="changePassword.captchaResponse" error="changePasswordForm.captchaResponse.$error"></error-messages>
+ </div> + + <div class="form-action-container"> + <button + ng-disabled="changePasswordForm.$invalid" + ng-click="goChangePassword()" + type="submit" + class="main-action">Change Password</button> + </div> + + </form> +</div> + +<div class="footer"></div> +<spinner spin="shouldSpin()"></spinner> + ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/changepasswordcontroller.js Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,179 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +angular.module('haikudepotserver').controller( + 'ChangePasswordController', + [ + '$scope','$log','$location','$routeParams', + 'jsonRpc','constants','breadcrumbs','userState', + function( + $scope,$log,$location,$routeParams, + jsonRpc,constants,breadcrumbs,userState) { + + $scope.breadcrumbItems = undefined; + $scope.user = undefined; + $scope.captchaToken = undefined; + $scope.captchaImageUrl = undefined; + $scope.changePasswordData = { + captchaResponse : undefined, + oldPasswordClear : undefined, + newPasswordClear : undefined, + newPasswordClearRepeated : undefined + }; + + var amChangingPassword = false; + + $scope.shouldSpin = function() { + return undefined == $scope.user || amChangingPassword; + } + + $scope.deriveFormControlsContainerClasses = function(name) {+ return $scope.changePasswordForm[name].$invalid ? ['form-control-group-error'] : [];
+ } + + refreshUser(); + regenerateCaptcha(); + + function refreshBreadcrumbItems() { + $scope.breadcrumbItems = [ + breadcrumbs.createViewUser($scope.user), + breadcrumbs.createChangePassword($scope.user) + ]; + } + + function refreshUser() { + jsonRpc.call( + constants.ENDPOINT_API_V1_USER, + "getUser", + [{ nickname : $routeParams.nickname }] + ).then( + function(result) { + $scope.user = result; + refreshBreadcrumbItems(); + $log.info('fetched user; '+result.nickname); + }, + function(err) {+ constants.ERRORHANDLING_JSONRPC(err,$location,$log);
+ } + ); + }; + + function regenerateCaptcha() { + + $scope.captchaToken = undefined; + $scope.captchaImageUrl = undefined; + $scope.changePasswordData.captchaResponse = undefined; + + jsonRpc.call( + constants.ENDPOINT_API_V1_CAPTCHA, + "generateCaptcha", + [{}] + ).then( + function(result) { + $scope.captchaToken = result.token;+ $scope.captchaImageUrl = 'data:image/png;base64,'+result.pngImageDataBase64;
+ refreshBreadcrumbItems(); + }, + function(err) {+ constants.ERRORHANDLING_JSONRPC(err,$location,$log);
+ } + ); + } ++ // When you go to action, if the user types the wrong captcha response then they will get an error message + // letting them know this, but there is no natural mechanism for this invalid state to get unset. For + // this reason, any change to the response text field will be taken to trigger this error state to be
+ // removed. + + $scope.captchaResponseDidChange = function() {+ $scope.changePasswordForm.captchaResponse.$setValidity('badresponse',true);
+ } + + $scope.newPasswordsChanged = function() {+ $scope.changePasswordForm.newPasswordClearRepeated.$setValidity(
+ 'repeat', + !$scope.changePasswordData.newPasswordClear+ | | !$scope.changePasswordData.newPasswordClearRepeated + || $scope.changePasswordData.newPasswordClear == $scope.changePasswordData.newPasswordClearRepeated);
+ } + + $scope.oldPasswordChanged = function() {+ $scope.changePasswordForm.oldPasswordClear.$setValidity('mismatched',true);
+ } + + $scope.goChangePassword = function() { + + if($scope.changePasswordForm.$invalid) {+ throw 'expected the change password of a user only to be possible if the form is valid';
+ } + + $scope.amChangingPassword = true; + + jsonRpc.call( + constants.ENDPOINT_API_V1_USER, + "changePassword", + [{ + nickname : $scope.user.nickname,+ oldPasswordClear : $scope.changePasswordData.oldPasswordClear, + newPasswordClear : $scope.changePasswordData.newPasswordClear,
+ captchaToken : $scope.captchaToken,+ captchaResponse : $scope.changePasswordData.captchaResponse
+ }] + ).then( + function(result) {+ $log.info('did change password for user; '+$scope.user.nickname);
+ userState.user(null); // logout + $location.path('/authenticateuser').search({ + nickname : $scope.user.nickname, + didChangePassword : 'true' + }); + }, + function(err) { + regenerateCaptcha(); + $scope.amSaving = false; + + switch(err.code) { ++ // should not be any validation failures that we need to deal with here.
+ + + case jsonRpc.errorCodes.VALIDATION: ++ // actually there shouldn't really be any validation problems except that the oldPasswordClear + // not match to the user for which the change password operation is being performed.
++ if(err.data && err.data.validationfailures) { + _.each(err.data.validationfailures, function(vf) { + var model = $scope.changePasswordForm[vf.property];
+ + if(model) {+ model.$setValidity(vf.message, false);
+ } + else {+ $log.error('other validation failures exist; will invoke default handling'); + constants.ERRORHANDLING_JSONRPC(err,$location,$log);
+ } + }) + } + + break; + + case jsonRpc.errorCodes.CAPTCHABADRESPONSE:+ $log.error('the user has mis-interpreted the captcha; will lodge an error into the form and then populate a new one for them'); + $scope.changePasswordForm.captchaResponse.$setValidity('badresponse',false);
+ break; + + default:+ constants.ERRORHANDLING_JSONRPC(err,$location,$log);
+ break; + } + } + ); + + } + + } + ] +); ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/userlabeldirective.js Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,24 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/**+ * <p>This directive renders a small piece of text and maybe a hyperlink to show a user.</p>
+ */ + +angular.module('haikudepotserver').directive('userLabel',function() { + return { + restrict: 'E', + template:'<span>{{user.nickname}}</span>', + replace: true, + scope: { + user: '=' + }, + controller: + ['$scope', + function($scope) { + } + ] + }; +}); ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/webapp/js/app/directive/validpassworddirective.js Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,29 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/**+ * <p>This directive will check to make sure that the password entered in is in the correct format, is long enough
+ * and is complex enough. + */ + +angular.module('haikudepotserver').directive('validPassword',function() { + return { + restrict: 'A', + require: 'ngModel', + link: function(scope,elem, attr, ctrl) { // ctrl = ngModel + + ctrl.$parsers.unshift(function(value) { + + var valid = (value.length >= 8) + && value.replace(/[^0-9]/g,'').length >= 2 + && value.replace(/[^A-Z]/g,'').length >= 1; + + ctrl.$setValidity('validPassword', valid); + return valid ? value : undefined; + }); + + } + }; +}); ======================================= --- /dev/null+++ /haikudepotserver-webapp/src/main/webapp/js/app/service/breadcrumbsservice.js Thu Jan 16 08:37:44 2014 UTC
@@ -0,0 +1,35 @@ +/* + * Copyright 2014, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ + +/** + * <p>This service helps in the creation of breadcrumb items.</p> + */ + +angular.module('haikudepotserver').factory('breadcrumbs', + [ + function() { + + var BreadcrumbsService = { + + createViewUser : function(user) { + return { + title : user.nickname, + path : '/viewuser/' + user.nickname + }; + }, + + createChangePassword : function(user) { + return { + title : 'Change Password', + path : '/changepassword/' + user.nickname + }; + } + }; + + return BreadcrumbsService; + + } + ] +); =======================================--- /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java Wed Nov 20 10:28:55 2013 UTC +++ /haikudepotserver-api1/src/main/java/org/haikuos/haikudepotserver/api1/UserApi.java Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014, Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -9,6 +9,7 @@ import org.haikuos.haikudepotserver.api1.model.user.*;import org.haikuos.haikudepotserver.api1.support.AuthorizationFailureException;
import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException; +import org.haikuos.haikudepotserver.api1.support.ValidationException; /*** <p>This interface defines operations that can be undertaken around users.</p>
@@ -40,4 +41,10 @@AuthenticateUserResult authenticateUser(AuthenticateUserRequest authenticateUserRequest);
+ /**+ * <p>This method will allow the client to modify the password of a user.</p>
+ */ ++ ChangePasswordResult changePassword(ChangePasswordRequest changePasswordRequest) throws ObjectNotFoundException, AuthorizationFailureException, ValidationException;
+ } =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java Wed Dec 11 08:25:33 2013 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/api1/UserApiImpl.java Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014 Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -8,14 +8,11 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.hash.Hashing; import com.google.common.util.concurrent.Uninterruptibles; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.configuration.server.ServerRuntime; import org.haikuos.haikudepotserver.api1.model.user.*;-import org.haikuos.haikudepotserver.api1.support.AuthorizationFailureException; -import org.haikuos.haikudepotserver.api1.support.CaptchaBadResponseException;
-import org.haikuos.haikudepotserver.api1.support.ObjectNotFoundException; +import org.haikuos.haikudepotserver.api1.support.*; import org.haikuos.haikudepotserver.captcha.CaptchaService; import org.haikuos.haikudepotserver.dataobjects.User; import org.haikuos.haikudepotserver.security.AuthenticationService; @@ -44,8 +41,14 @@public CreateUserResult createUser(CreateUserRequest createUserRequest) {
Preconditions.checkNotNull(createUserRequest); - Preconditions.checkNotNull(createUserRequest.captchaToken); - Preconditions.checkNotNull(createUserRequest.captchaResponse);+ Preconditions.checkNotNull(!Strings.isNullOrEmpty(createUserRequest.nickname)); + Preconditions.checkNotNull(!Strings.isNullOrEmpty(createUserRequest.passwordClear)); + Preconditions.checkNotNull(!Strings.isNullOrEmpty(createUserRequest.captchaToken)); + Preconditions.checkNotNull(!Strings.isNullOrEmpty(createUserRequest.captchaResponse));
++ if(!authenticationService.validatePassword(createUserRequest.passwordClear)) { + throw new ValidationException(new ValidationFailure("passwordClear", "invalid"));
+ } // check the supplied catcha matches the token. @@ -77,7 +80,7 @@ User user = context.newObject(User.class); user.setNickname(createUserRequest.nickname); user.setPasswordSalt(); // random- user.setPasswordHash(Hashing.sha256().hashUnencodedChars(user.getPasswordSalt() + createUserRequest.passwordClear).toString()); + user.setPasswordHash(authenticationService.hashPassword(user, createUserRequest.passwordClear));
context.commitChanges(); logger.info("data create user; {}",user.getNickname()); @@ -125,8 +128,8 @@ }authenticateUserResult.authenticated = authenticationService.authenticate(
- authenticateUserRequest.nickname, - authenticateUserRequest.passwordClear).isPresent(); + authenticateUserRequest.nickname, + authenticateUserRequest.passwordClear).isPresent(); // if the authentication has failed then best to sleep for a moment // to make brute forcing a bit more tricky. @@ -137,6 +140,89 @@ return authenticateUserResult; } + + @Override + public ChangePasswordResult changePassword( + ChangePasswordRequest changePasswordRequest)+ throws ObjectNotFoundException, AuthorizationFailureException, ValidationException {
+ + Preconditions.checkNotNull(changePasswordRequest);+ Preconditions.checkState(!Strings.isNullOrEmpty(changePasswordRequest.nickname)); + Preconditions.checkState(!Strings.isNullOrEmpty(changePasswordRequest.newPasswordClear));
++ if(!authenticationService.validatePassword(changePasswordRequest.newPasswordClear)) { + throw new ValidationException(new ValidationFailure("passwordClear", "invalid"));
+ } + + final ObjectContext context = serverRuntime.getContext(); + + User authUser = obtainAuthenticatedUser(context); ++ // if the logged in user is non-root then we need to make sure that the captcha
+ // is valid. + + if(!authUser.getIsRoot()) { + + if(Strings.isNullOrEmpty(changePasswordRequest.captchaToken)) {+ throw new IllegalStateException("the captcha token must be supplied to change the password");
+ } ++ if(Strings.isNullOrEmpty(changePasswordRequest.captchaResponse)) { + throw new IllegalStateException("the captcha response must be supplied to change the password");
+ } ++ if(!captchaService.verify(changePasswordRequest.captchaToken, changePasswordRequest.captchaResponse)) {
+ throw new CaptchaBadResponseException(); + } + } ++ // if the logged in user is not root then only the user who has authenticated can change their password.
++ if(!authUser.getNickname().equals(changePasswordRequest.nickname)) {
+ if(authUser.getIsRoot()) { + logger.info("allowing change password for root user"); + } + else {+ logger.info("the logged in user {} is not allowed to change the password of another user {}",authUser.getNickname(),changePasswordRequest.nickname);
+ throw new AuthorizationFailureException(); + } + } ++ // if the logged in user is non-root then we need to make sure that the old and new passwords
+ // match-up. + + if(!authUser.getIsRoot()) { ++ if(Strings.isNullOrEmpty(changePasswordRequest.oldPasswordClear)) { + throw new IllegalStateException("the old password clear is required to change the password of a user unless the logged in user is root.");
+ } + + if(!authenticationService.authenticate( + changePasswordRequest.nickname, + changePasswordRequest.oldPasswordClear).isPresent()) { ++ // if the old password does not match to the user then we should present this + // as a validation failure rather than an authorization failure.
++ logger.info("the supplied old password is invalid for the user {}", changePasswordRequest.nickname);
++ throw new ValidationException(new ValidationFailure("oldPasswordClear","mismatched"));
+ } + } ++ Optional<User> userOptional = User.getByNickname(context, changePasswordRequest.nickname);
+ + if(!userOptional.isPresent()) {+ throw new ObjectNotFoundException(User.class.getSimpleName(), changePasswordRequest.nickname);
+ } + + User user = userOptional.get();+ user.setPasswordHash(authenticationService.hashPassword(user, changePasswordRequest.newPasswordClear));
+ context.commitChanges();+ logger.info("did change password for user {}", changePasswordRequest.nickname);
+ + return new ChangePasswordResult(); + } } =======================================--- /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthenticationService.java Wed Dec 11 08:25:33 2013 UTC +++ /haikudepotserver-webapp/src/main/java/org/haikuos/haikudepotserver/security/AuthenticationService.java Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014, Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -100,7 +100,7 @@ if(userOptional.isPresent()) { User user = userOptional.get();- String hash = Hashing.sha256().hashUnencodedChars(user.getPasswordSalt() + passwordClear).toString();
+ String hash = hashPassword(user, passwordClear); if(hash.equals(user.getPasswordHash())) {result = Optional.fromNullable(userOptional.get().getObjectId());
@@ -119,5 +119,57 @@ return result; } + + /**+ * <p>This method will hash the password in a consistent manner across the whole system.</p>
+ */ + + public String hashPassword(User user, String passwordClear) {+ return Hashing.sha256().hashUnencodedChars(user.getPasswordSalt() + passwordClear).toString();
+ } + + private int countMatches(String s, CharToBooleanFunction fn) { + int length = s.length(); + int count = 0; + for(int i=0;i<length;i++) { + char c = s.charAt(i); + if(fn.test(c)) { + count++; + } + } + return count; + } + + /**+ * <p>Passwords should be hard to guess and so there needs to be a certain level of complexity to + * them. They should of a certain length and should contain some mix of letters and digits as well
+ * as at least one upper case letter.</p> + * + * <p>This method will check the password for suitability.</p> + */ + + public boolean validatePassword(String passwordClear) { + Preconditions.checkNotNull(passwordClear); + + if(passwordClear.length() < 8) { + return false; + } + + // get a count of digits - should be at least two.+ if(countMatches(passwordClear, new CharToBooleanFunction() { public boolean test(char c) { return c >= 48 && c <= 57; } }) < 2) {
+ return false; + } + + // get a count of upper case letters - should be at least one.+ if(countMatches(passwordClear, new CharToBooleanFunction() { public boolean test(char c) { return c >= 65 && c <= 90; } }) < 1) {
+ return false; + } + + return true; + } + + private interface CharToBooleanFunction { + boolean test(char c); + } } =======================================--- /haikudepotserver-webapp/src/main/resources/messages.properties Mon Jan 13 10:50:43 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/messages.properties Thu Jan 16 08:37:44 2014 UTC
@@ -4,9 +4,19 @@createUser.nickname.pattern=The nickname can consist of latin characters and digits only and must be between 4 and 16 characters in length. createUser.nickname.notunique=The nickname is already in-use; nominate an alternative nickname.
createUser.passwordClear.required=The password of the new user is required.-createUser.passwordClear.minlength=The password must be at least 5 characters long. -createUserForm.captchaResponse.required=The response to the question in the image is required to ensure that the registration is from a human operator. -createUserForm.captchaResponse.badresponse=The response supplied does not match the question in the image or the question has expired; a new image has been provided. +createUser.passwordClear.validPassword=The password should be at least 8 characters long and contain two digits and one uppercase latin letter. +createUser.passwordClearRepeated.required=The password must be repeated to ensure it was entered correctly. +createUser.passwordClearRepeated.repeat=The password has not been repeated correctly. +createUser.captchaResponse.required=The response to the question in the image is required to ensure that the registration is from a human operator. +createUser.captchaResponse.badresponse=The response supplied does not match the question in the image or the question has expired; a new image has been provided.
++changePassword.oldPasswordClear.required=The existing password for this user is required to prove the user's identity. +changePassword.newPasswordClear.required=The new password is required to set the password. +changePassword.newPasswordClear.validPassword=The password should be at least 8 characters long and contain two digits and one uppercase latin letter. +changePassword.newPasswordClearRepeated.required=The new password must be repeated in order to ensure that it has been supplied correctly. +changePassword.newPasswordClearRepeated.repeat=The password has not been repeated correctly. +changePassword.captchaResponse.required=The result of this simple question must be supplied to prove that the change password is from a human operator. +changePassword.oldPasswordClear.mismatched=Authentication problem; try to enter the existing password again.
authenticateUser.nickname.required=The nickname is required to login. authenticateUser.passwordClear.required=The password is required to login. =======================================--- /haikudepotserver-webapp/src/main/resources/spring/webresourcegroup-context.xml Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/resources/spring/webresourcegroup-context.xml Thu Jan 16 08:37:44 2014 UTC
@@ -55,6 +55,8 @@<value>/js/app/directive/pkgicondirective.js</value> <value>/js/app/directive/pkglabeldirective.js</value> <value>/js/app/directive/filesupplydirective.js</value> + <value>/js/app/directive/validpassworddirective.js</value> + <value>/js/app/directive/userlabeldirective.js</value>
<value>/js/app/controller/viewpkgcontroller.js</value> <value>/js/app/controller/homecontroller.js</value>
@@ -63,12 +65,14 @@<value>/js/app/controller/viewusercontroller.js</value> <value>/js/app/controller/authenticateusercontroller.js</value> <value>/js/app/controller/editpkgiconcontroller.js</value> + <value>/js/app/controller/changepasswordcontroller.js</value>
<value>/js/app/service/jsonrpcservice.js</value> <value>/js/app/service/messagesourceservice.js</value> <value>/js/app/service/userstateservice.js</value> <value>/js/app/service/referencedataservice.js</value> <value>/js/app/service/pkgiconservice.js</value> + <value>/js/app/service/breadcrumbsservice.js</value>
</list> </property> =======================================--- /haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/css/haikudepotserver.css Thu Jan 16 08:37:44 2014 UTC
@@ -140,7 +140,7 @@ */ .form-control-group .form-control-group-static { - padding: 8px; + padding: 3px; } .form-control-group.form-control-group-error input { =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateuser.html Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateuser.html Thu Jan 16 08:37:44 2014 UTC
@@ -2,6 +2,10 @@ <div class="content-container"> + <div class="form-info-container" ng-show="didChangePassword">+ The password was changed; you can now re-authenticate with the new password.
+ </div> + <div class="form-info-container" ng-show="didCreate"> The new account was created; you can now login and use it. </div> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateusercontroller.js Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/authenticateusercontroller.js Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014, Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -19,6 +19,7 @@ $scope.didFailAuthentication = false; $scope.amAuthenticating = false; $scope.didCreate = !!$location.search()['didCreate'];+ $scope.didChangePassword = !!$location.search()['didChangePassword'];
$scope.authenticationDetails = { nickname : undefined, passwordClear : undefined =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/createuser.html Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/createuser.html Thu Jan 16 08:37:44 2014 UTC
@@ -24,24 +24,37 @@ type="password" name="passwordClear" ng-required="true" - ng-minlength="5" + ng-change="passwordsChanged()" + valid-password="" ng-model="newUser.passwordClear"></input><error-messages key-prefix="createUser.passwordClear" error="createUserForm.passwordClear.$error"></error-messages>
</div>- <label for="create-user-captcha-response-input">Check for a Person</label>
+ <label for="password-clear-repeated">Password Repeated</label>+ <div class="form-control-group" ng-class="deriveFormControlsContainerClasses('passwordClearRepeated')">
+ <input + id="password-clear-repeated" + type="password" + name="passwordClearRepeated" + ng-change="passwordsChanged()" + ng-required="true" + ng-model="newUser.passwordClearRepeated"></input>+ <error-messages key-prefix="createUser.passwordClearRepeated" error="createUserForm.passwordClearRepeated.$error"></error-messages>
+ </div> + + <label for="captcha-response-input">Check for a Person</label><div class="form-control-group" ng-class="deriveFormControlsContainerClasses('captchaResponse')"> <img style="vertical-align:middle;" src="{{captchaImageUrl}}"></img>
= <input - id="create-user-captcha-response-input" + id="captcha-response-input" size="3" type="text" name="captchaResponse" ng-required="true" ng-change="captchaResponseDidChange()" ng-model="newUser.captchaResponse"></input>- <error-messages key-prefix="createUserForm.captchaResponse" error="createUserForm.captchaResponse.$error"></error-messages> + <error-messages key-prefix="createUser.captchaResponse" error="createUserForm.captchaResponse.$error"></error-messages>
</div> <div class="form-action-container"> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/createusercontroller.js Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/createusercontroller.js Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014, Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -19,6 +19,7 @@ $scope.newUser = { nickname : undefined, passwordClear : undefined, + passwordClearRepeated : undefined, captchaResponse : undefined }; @@ -73,6 +74,14 @@ $scope.nicknameDidChange = function() {$scope.createUserForm.nickname.$setValidity('notunique',true);
} + + $scope.passwordsChanged = function() { + $scope.createUserForm.passwordClearRepeated.$setValidity( + 'repeat', + !$scope.newUser.passwordClear + || !$scope.newUser.passwordClearRepeated+ || $scope.newUser.passwordClear == $scope.newUser.passwordClearRepeated);
+ }// This function will take the data from the form and will create the user from this data.
@@ -115,19 +124,17 @@ // default handler.if(err.data && err.data.validationfailures) { - var nicknameFailure = _.find(err.data.validationfailures, function(e) { return e.property == 'nickname'; }); + _.each(err.data.validationfailures, function(vf) { + var model = $scope.createUserForm[vf.property];
- if(nicknameFailure) {- $scope.createUserForm.nickname.$setValidity(nicknameFailure.message,false);
- } - } - - if( - !err.data - || !err.data.validationfailures- || 0!=_.filter(err.data.validationfailures, function(e) { return e.property != 'nickname'; }).length) { - $log.error('other validation failures exist; will invoke default handling'); - constants.ERRORHANDLING_JSONRPC(err,$location,$log);
+ if(model) {+ model.$setValidity(vf.message, false);
+ } + else {+ $log.error('other validation failures exist; will invoke default handling'); + constants.ERRORHANDLING_JSONRPC(err,$location,$log);
+ } + }) } break; =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgicon.html Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/editpkgicon.html Thu Jan 16 08:37:44 2014 UTC
@@ -7,8 +7,8 @@ <label>Package</label> <div class="form-control-group"> <div class="form-control-group-static"> - <pkg-label pkg="pkg"></pkg-label> - </div> + <pkg-label pkg="pkg"></pkg-label> + </div> </div> <label for="icon-32-file">Icon 32x32px</label> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewuser.html Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewuser.html Thu Jan 16 08:37:44 2014 UTC
@@ -6,6 +6,12 @@ {{user.nickname}} </h1> + <h2>Actions</h2> + + <ul>+ <li><a href="" ng-click="goChangePassword()">Change password</a></li>
+ </ul> + </div> <div class="footer"></div> =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewusercontroller.js Fri Nov 15 08:51:45 2013 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/controller/viewusercontroller.js Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014, Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -47,6 +47,10 @@ ); }; + $scope.goChangePassword = function() { + $location.path('/changepassword/' + $scope.user.nickname); + } + } ] ); =======================================--- /haikudepotserver-webapp/src/main/webapp/js/app/routes.js Tue Jan 14 09:30:30 2014 UTC +++ /haikudepotserver-webapp/src/main/webapp/js/app/routes.js Thu Jan 16 08:37:44 2014 UTC
@@ -1,5 +1,5 @@ /* - * Copyright 2013, Andrew Lindesay + * Copyright 2013-2014, Andrew Lindesay * Distributed under the terms of the MIT License. */ @@ -10,6 +10,7 @@ $routeProvider.when('/authenticateuser',{controller:'AuthenticateUserController', templateUrl:'/js/app/controller/authenticateuser.html'}) .when('/createuser',{controller:'CreateUserController', templateUrl:'/js/app/controller/createuser.html'}) + .when('/changepassword/:nickname',{controller:'ChangePasswordController', templateUrl:'/js/app/controller/changepassword.html'}) .when('/viewuser/:nickname',{controller:'ViewUserController', templateUrl:'/js/app/controller/viewuser.html'}) .when('/viewpkg/:name/:version/:architectureCode',{controller:'ViewPkgController', templateUrl:'/js/app/controller/viewpkg.html'}) .when('/editpkgicon/:name',{controller:'EditPkgIconController', templateUrl:'/js/app/controller/editpkgicon.html'})