LT-97: Add requireRegexValidation and requireStrongPasswords properties under passwordOptions which is under passwordOptions.
- requireRegexValidation toggles whether or not password validation is controlled by regular expression - requireStrongPasswords toggles whether password validation is controlled by zxcvbn package LT-104: User account shall be locked after 5 failed attempts - "failedAttemptsLimit" property which is under passwordOptions in ActiveEntry configuration object sets number of failed attempts count to lock user account, it is set 5 as default LT-99: Passwords shall use password history of 6 - "passwordHistoryCount" property which is under passwordOptions in ActiveEntry configuration object sets count of last passwords that is not used to reset password - Show error messages in changePassword template - Make signIn button disabled if inputs are not validated
This commit is contained in:
parent
24e615da1e
commit
eb32e1771f
@ -15,10 +15,13 @@ if (Meteor.isClient){
|
||||
primary: ""
|
||||
},
|
||||
passwordOptions: {
|
||||
showPasswordStrengthIndicator: true,
|
||||
requireRegexValidation: true
|
||||
showPasswordStrengthIndicator: false,
|
||||
requireRegexValidation: true,
|
||||
//requireStrongPasswords: false
|
||||
passwordHistoryCount: 6,
|
||||
failedAttemptsLimit: 5
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
<ul class="nav nav-pills pull-right">
|
||||
<li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">{{fullName}}<b class="caret"></b></a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a href="#" id="logoutButton">Logout</a></li>
|
||||
<li><a href="changePassword" id="changePassword"><i class="fa fa-lock"></i>Change Password</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" id="logoutButton"><i class="fa fa-power-off"></i>Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -3,6 +3,9 @@ Template.layoutLesionTracker.events({
|
||||
Meteor.logout(function(){
|
||||
Router.go('/entrySignIn');
|
||||
});
|
||||
},
|
||||
'click #changePassword': function() {
|
||||
Router.go('/changePassword');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -11,9 +11,26 @@
|
||||
position: relative
|
||||
float: right
|
||||
|
||||
.nav-pills > li > a:hover, .nav .open > a, .nav .open > a:hover, .nav .open > a:focus
|
||||
background-color: #eee
|
||||
color: black
|
||||
|
||||
.dropdown-menu > li > a:hover
|
||||
background-color: #eee
|
||||
|
||||
.dropdown-menu > li > a >i
|
||||
margin-right: 5px
|
||||
|
||||
.dropdown-toggle
|
||||
color: #C1C1C1
|
||||
|
||||
.divider
|
||||
height: 2px
|
||||
margin: 9px 2px
|
||||
overflow: hidden
|
||||
background-color: #e5e5e5
|
||||
border-bottom: 1px solid #fff
|
||||
|
||||
.navbar-brand
|
||||
display: inline-block
|
||||
padding: 0
|
||||
|
||||
@ -8,6 +8,14 @@
|
||||
<div id="changePasswordPageMessage" class="subtitle-auth" style="{{getChangePasswordMessageColor}}">{{getChangePasswordMessage}}</div>
|
||||
|
||||
<form>
|
||||
{{#if entryErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each entryErrorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="changePasswordPageOldPasswordInput" type="password" name="oldPassword" placeholder="Old Password" style="{{getPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
|
||||
@ -44,9 +44,20 @@ Template.changePassword.helpers({
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
|
||||
entryErrorMessages: function() {
|
||||
var errorMessages = [];
|
||||
Object.keys(ActiveEntry.errorMessages.all()).forEach(function(key) {
|
||||
if ((key === "password" || key === "confirm") && ActiveEntry.errorMessages.get(key)) {
|
||||
errorMessages.push(ActiveEntry.errorMessages.get(key));
|
||||
}
|
||||
});
|
||||
return errorMessages;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.changePassword.events({
|
||||
'change, keyup #changePasswordPagePasswordInput': function (event, template) {
|
||||
var password = $('[name="password"]').val();
|
||||
@ -73,12 +84,37 @@ Template.changePassword.events({
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
|
||||
Accounts.changePassword(oldPassword, confirmPassword, function(error) {
|
||||
if (error) {
|
||||
console.warn(error);
|
||||
return;
|
||||
}
|
||||
console.log('Password changed!');
|
||||
});
|
||||
|
||||
if (ActiveEntry.successMessages.get('password') && ActiveEntry.successMessages.get('confirm') && oldPassword) {
|
||||
Meteor.call("checkPasswordExistence", new String(password).hashCode(), function(error, result) {
|
||||
if (error) {
|
||||
console.warn(error.message);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', error.message);
|
||||
|
||||
} else {
|
||||
if (result) {
|
||||
ActiveEntry.errorMessages.set('changePasswordError', 'Password is used before. Please change your new password.');
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
|
||||
// If password is not found in password history, change the password
|
||||
Accounts.changePassword(oldPassword, confirmPassword, function(error) {
|
||||
if (error) {
|
||||
console.warn(error);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', error.message);
|
||||
} else {
|
||||
// Save the new password
|
||||
ActiveEntry.insertHashedPassword(confirmPassword);
|
||||
// Logout
|
||||
ActiveEntry.signOut();
|
||||
// Go to signIn page for new entry
|
||||
Router.go('/entrySignIn');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
</form>
|
||||
|
||||
<br><br>
|
||||
<button id="signInToAppButton" class="btn-primary btn-main btn-large" style="{{getButtonColor}}">{{getButtonText}}</button>
|
||||
<button id="signInToAppButton" class="btn-primary btn-main btn-large disabledButton" disabled style="{{getButtonColor}}">{{getButtonText}}</button>
|
||||
|
||||
<br><br>
|
||||
<button id="needAnAccountButton" class="btn-gray btn-main btn-large">Need an account?</button>
|
||||
|
||||
@ -65,8 +65,8 @@ Template.entrySignIn.helpers({
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
//==================================================================================================
|
||||
// COMPONENT OUTPUTS
|
||||
@ -90,24 +90,28 @@ Template.entrySignIn.events({
|
||||
|
||||
ActiveEntry.verifyEmail(email);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'change input[name="email"]': function (event, template) {
|
||||
var email = $('input[name="email"]').val();
|
||||
|
||||
ActiveEntry.verifyEmail(email);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'keyup #signInPagePasswordInput': function (event, template) {
|
||||
var password = $('input[name="password"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
'change #signInPagePasswordInput': function (event, template) {
|
||||
var password = $('input[name="password"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
setSignInButtonStyling();
|
||||
},
|
||||
// 'submit': function (event, template) {
|
||||
// event.preventDefault();
|
||||
@ -128,13 +132,7 @@ Template.entrySignIn.events({
|
||||
},
|
||||
'keyup #entrySignIn': function(event, template) {
|
||||
if(event.keyCode == 13) {
|
||||
ActiveEntry.verifyEmail($("#signInPageEmailInput").val());
|
||||
|
||||
if (!ActiveEntry.errorMessages.get('signInError') &&
|
||||
ActiveEntry.successMessages.get('email') &&
|
||||
$("#signInPagePasswordInput").val()) {
|
||||
$("#signInToAppButton").click();
|
||||
}
|
||||
$("#signInToAppButton").click();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -142,3 +140,18 @@ Template.entrySignIn.events({
|
||||
|
||||
|
||||
//==================================================================================================
|
||||
|
||||
// Sets SignInButton Styling according to email and password fields
|
||||
function setSignInButtonStyling() {
|
||||
var signInToAppButton = $("#signInToAppButton");
|
||||
if ($("#signInPagePasswordInput").val() && ActiveEntry.successMessages.get('email')) {
|
||||
// Set button as enable
|
||||
signInToAppButton.removeClass("disabledButton");
|
||||
signInToAppButton.attr("disabled", false);
|
||||
} else {
|
||||
signInToAppButton.addClass("disabledButton");
|
||||
signInToAppButton.attr("disabled", true);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -14,4 +14,18 @@
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.disabledButton {
|
||||
border: none;
|
||||
background-image: none;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
filter: alpha(opacity=40);
|
||||
-khtml-opacity: 0.40;
|
||||
-moz-opacity: 0.40;
|
||||
opacity: 0.40;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -11,13 +11,6 @@
|
||||
<div id="signUpPageMessage" class="subtitle-auth" style="{{getSignUpMessageColor}}">{{getSignUpMessage}}</div>
|
||||
|
||||
<form id="entrySignUpForm">
|
||||
<!--{{#if errorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each errorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}-->
|
||||
|
||||
{{#if entryErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
|
||||
@ -28,6 +28,7 @@ if (Meteor.isClient) {
|
||||
requireRegexValidation: true
|
||||
//requireStrongPasswords: false
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -62,7 +63,6 @@ ActiveEntry.verifyPassword = function (password) {
|
||||
ActiveEntry.errorMessages.set('password', null);
|
||||
ActiveEntry.successMessages.set('password', 'Password present');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
ActiveEntry.verifyConfirmPassword = function (password, confirmPassword) {
|
||||
@ -88,7 +88,6 @@ ActiveEntry.verifyEmail = function (email) {
|
||||
ActiveEntry.errorMessages.set('email', 'Email is poorly formatted');
|
||||
ActiveEntry.successMessages.set('email', null);
|
||||
} else if (email.indexOf("@") >= 0){
|
||||
//ActiveEntry.errorMessages.set('email', 'Email present');
|
||||
ActiveEntry.errorMessages.set('email', null);
|
||||
ActiveEntry.successMessages.set('email', 'Email present');
|
||||
}
|
||||
@ -111,17 +110,43 @@ ActiveEntry.verifyFullName = function (fullName) {
|
||||
ActiveEntry.signIn = function (emailValue, passwordValue){
|
||||
ActiveEntry.verifyPassword(passwordValue);
|
||||
ActiveEntry.verifyEmail(emailValue);
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
var failedAttemptsLimit = ActiveEntryConfig && ActiveEntryConfig.passwordOptions && ActiveEntryConfig.passwordOptions.failedAttemptsLimit || 5;
|
||||
|
||||
Meteor.loginWithPassword({email: emailValue}, passwordValue, function (error, result) {
|
||||
Meteor.call("getFailedAttemptsCount", emailValue, function(error, failedAttemptsCount) {
|
||||
if (error) {
|
||||
ActiveEntry.errorMessages.set('signInError', error.message);
|
||||
console.warn(error.message);
|
||||
} else {
|
||||
console.log('result', result);
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
console.log('ActiveEntryConfig', JSON.stringify(ActiveEntryConfig));
|
||||
Router.go(ActiveEntryConfig.signIn.destination);
|
||||
if (failedAttemptsCount != failedAttemptsLimit) {
|
||||
Meteor.loginWithPassword({email: emailValue}, passwordValue, function (error, result) {
|
||||
if (error) {
|
||||
// Login failed
|
||||
Meteor.call("updateFailedAttempts", [emailValue, failedAttemptsLimit], function(error, failedAttemptCount) {
|
||||
if (error) {
|
||||
console.warn(error);
|
||||
} else {
|
||||
if (failedAttemptCount == failedAttemptsLimit) {
|
||||
ActiveEntry.errorMessages.set('signInError', "Too many failed login attempts. Your account has been locked.");
|
||||
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('signInError', (failedAttemptsLimit - failedAttemptCount) + " attempts remaining.");
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('result', result);
|
||||
Meteor.call("resetFailedAttempts", emailValue);
|
||||
Router.go(ActiveEntryConfig.signIn.destination);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('signInError', "Your account has been locked.");
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullName){
|
||||
@ -147,12 +172,18 @@ ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullN
|
||||
password: passwordValue,
|
||||
profile: {
|
||||
fullName: fullName
|
||||
},
|
||||
testCase: {
|
||||
createdAt: new Date()
|
||||
}
|
||||
}, function (error, result) {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
ActiveEntry.errorMessages.set('signInError', error.message);
|
||||
} else {
|
||||
// Add password in previous password field
|
||||
ActiveEntry.insertHashedPassword(passwordValue);
|
||||
ActiveEntry.updatePasswordCreatedDate();
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
Router.go(ActiveEntryConfig.signUp.destination);
|
||||
}
|
||||
@ -172,6 +203,18 @@ ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullN
|
||||
// Router.go(ActiveEntryConfig.signIn.destination);
|
||||
// });
|
||||
};
|
||||
|
||||
// Insert hashed password in previousPasswords fields
|
||||
ActiveEntry.insertHashedPassword = function(passwordValue) {
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
var passwordHistoryCount = ActiveEntryConfig && ActiveEntryConfig.passwordOptions && ActiveEntryConfig.passwordOptions.passwordHistoryCount || 6;
|
||||
Meteor.call("insertHashedPassword", [new String(passwordValue).hashCode(),passwordHistoryCount]);
|
||||
};
|
||||
|
||||
ActiveEntry.updatePasswordCreatedDate = function() {
|
||||
Meteor.call("updatePasswordCreatedDate");
|
||||
};
|
||||
|
||||
ActiveEntry.signOut = function (){
|
||||
Meteor.logout();
|
||||
};
|
||||
|
||||
10
Packages/active-entry/lib/hashCodeGenerator.js
Normal file
10
Packages/active-entry/lib/hashCodeGenerator.js
Normal file
@ -0,0 +1,10 @@
|
||||
String.prototype.hashCode = function() {
|
||||
var hash = 0, i, chr, len;
|
||||
if (this.length === 0) return hash;
|
||||
for (i = 0, len = this.length; i < len; i++) {
|
||||
chr = this.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
@ -35,7 +35,8 @@ Package.onUse(function (api) {
|
||||
|
||||
api.addFiles([
|
||||
'lib/jquery.pwstrength.bootstrap.js',
|
||||
'lib/checkPasswordStrength.js'
|
||||
'lib/checkPasswordStrength.js',
|
||||
'lib/hashCodeGenerator.js'
|
||||
], ['client']);
|
||||
|
||||
api.imply('accounts-base');
|
||||
|
||||
@ -3,10 +3,74 @@ Meteor.methods({
|
||||
console.log('Initializing Users', Meteor.users.find().fetch());
|
||||
|
||||
},
|
||||
|
||||
dropEntryUsers: function (){
|
||||
console.log('Drop Users', Meteor.users.find().fetch());
|
||||
Meteor.users.find().forEach(function(user){
|
||||
Meteor.users.remove({_id: user._id});
|
||||
});
|
||||
},
|
||||
|
||||
insertHashedPassword: function(passwordParameters) {
|
||||
var hashedPassword = passwordParameters[0];
|
||||
var passwordHistoryCount = passwordParameters[1];
|
||||
|
||||
var userId = Meteor.userId();
|
||||
var previousPasswords = Meteor.users.findOne({_id: userId}).previousPasswords;
|
||||
if (previousPasswords) {
|
||||
if (previousPasswords.length == passwordHistoryCount) {
|
||||
// Remove oldest password
|
||||
var ascSortedPasswords = _.sortBy(previousPasswords, function(previousPassword){ return previousPassword.createdAt; });
|
||||
ascSortedPasswords.splice(0, 1);
|
||||
previousPasswords = ascSortedPasswords;
|
||||
}
|
||||
|
||||
previousPasswords.push({hashedPassword: hashedPassword, createdAt: new Date()});
|
||||
Meteor.users.update({_id: userId}, {$set: {previousPasswords: previousPasswords}});
|
||||
} else {
|
||||
Meteor.users.update({_id: userId}, {$set: {previousPasswords: [{hashedPassword: hashedPassword, createdAt: new Date(), select: false}]}});
|
||||
}
|
||||
},
|
||||
|
||||
checkPasswordExistence: function(hashedPassword) {
|
||||
var previousPasswords = Meteor.users.find({_id: Meteor.userId()}).fetch()[0].previousPasswords;
|
||||
for(var i=0; i< previousPasswords.length; i++) {
|
||||
var recordedHashedPassword = previousPasswords[i].hashedPassword;
|
||||
if (recordedHashedPassword == hashedPassword) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
},
|
||||
|
||||
getFailedAttemptsCount: function(emailAddress) {
|
||||
return Meteor.users.findOne({"emails.address": emailAddress}).failedPasswordAttempts || 0;
|
||||
},
|
||||
|
||||
updateFailedAttempts: function(failedAttemptsParameters) {
|
||||
var emailAddress = failedAttemptsParameters[0];
|
||||
var failedAttemptsLimit = failedAttemptsParameters[1];
|
||||
var failedAttemptCount = Meteor.users.findOne({"emails.address": emailAddress}).failedPasswordAttempts || 0;
|
||||
if (failedAttemptCount == failedAttemptsLimit) {
|
||||
return failedAttemptCount;
|
||||
} else {
|
||||
if (failedAttemptCount == 4) {
|
||||
// Locked user account
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {"profile.isLocked": true, failedPasswordAttempts: failedAttemptCount + 1}});
|
||||
} else if (failedAttemptCount < 4) {
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {failedPasswordAttempts: failedAttemptCount + 1}});
|
||||
}
|
||||
}
|
||||
|
||||
return failedAttemptCount + 1;
|
||||
},
|
||||
|
||||
resetFailedAttempts: function(emailAddress) {
|
||||
Meteor.users.update({"emails.address": emailAddress}, {$set: {failedPasswordAttempts: 0}});
|
||||
},
|
||||
|
||||
updatePasswordCreatedDate: function() {
|
||||
Meteor.users.update({_id: Meteor.userId()}, {$set: {"services.password.createdAt": new Date()}});
|
||||
}
|
||||
});
|
||||
|
||||
@ -41,19 +41,13 @@ Meteor.startup(function() {
|
||||
overdueTimestamp = overdueTimestamp - (overdueTimestamp % 1000);
|
||||
console.log(overdueTimestamp);
|
||||
var startTime = inactivityTimeout - dialogTimeout;
|
||||
if (overdueTimestamp <= inactivityTimeout) {
|
||||
var nextIntervalTime = overdueTimestamp + countdownHeartbeatInterval;
|
||||
if (nextIntervalTime <= inactivityTimeout && nextIntervalTime >= startTime) {
|
||||
if (Math.abs(startTime - overdueTimestamp) <= Math.abs(nextIntervalTime - startTime) && !dialogIsOpen) {
|
||||
// Open dialog
|
||||
var leftTime = Math.round((inactivityTimeout - overdueTimestamp) / 1000);
|
||||
$.event.trigger('TriggerOpenTimeoutCountdownDialog', leftTime);
|
||||
dialogIsOpen = true;
|
||||
}
|
||||
} else {
|
||||
// Event to close dialog
|
||||
$.event.trigger('TriggerCloseTimeoutCountdownDialog');
|
||||
dialogIsOpen = false;
|
||||
var nextIntervalTime = overdueTimestamp + countdownHeartbeatInterval;
|
||||
if (overdueTimestamp <= inactivityTimeout && nextIntervalTime <= inactivityTimeout && nextIntervalTime >= startTime) {
|
||||
if (Math.abs(startTime - overdueTimestamp) <= Math.abs(nextIntervalTime - startTime) && !dialogIsOpen) {
|
||||
// Open dialog
|
||||
var leftTime = Math.round((inactivityTimeout - overdueTimestamp) / 1000);
|
||||
$.event.trigger('TriggerOpenTimeoutCountdownDialog', leftTime);
|
||||
dialogIsOpen = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user