Merge userManagement branch to master
This commit is contained in:
commit
3b7330ca75
@ -49,3 +49,4 @@ clinical:hipaa-logger
|
||||
anti:gagarin@=0.4.11
|
||||
check
|
||||
aldeed:template-extension@4.0.0
|
||||
zuuk:stale-session
|
||||
|
||||
@ -17,10 +17,10 @@ caching-compiler@1.0.0
|
||||
caching-html-compiler@1.0.2
|
||||
callback-hook@1.0.4
|
||||
check@1.1.0
|
||||
clinical:active-entry@1.5.14
|
||||
clinical:active-entry@1.5.16
|
||||
clinical:auto-resizing@0.1.2
|
||||
clinical:error-pages@0.1.1
|
||||
clinical:extended-api@2.2.1
|
||||
clinical:extended-api@2.2.2
|
||||
clinical:fonts@1.0.0
|
||||
clinical:hipaa-audit-log@2.4.2
|
||||
clinical:hipaa-logger@1.0.1
|
||||
@ -28,7 +28,7 @@ clinical:router@2.0.17
|
||||
clinical:router-location@2.0.14
|
||||
clinical:router-middleware-stack@2.0.13
|
||||
clinical:router-url@2.0.15
|
||||
clinical:theming@0.3.1
|
||||
clinical:theming@0.4.7
|
||||
coffeescript@1.0.11
|
||||
cornerstone@0.0.1
|
||||
ddp@1.2.2
|
||||
@ -46,7 +46,7 @@ ejson@1.0.7
|
||||
email@1.0.8
|
||||
es5-shim@4.1.14
|
||||
fastclick@1.0.7
|
||||
fortawesome:fontawesome@4.4.0
|
||||
fortawesome:fontawesome@4.5.0
|
||||
geojson-utils@1.0.4
|
||||
grove:less@0.1.1
|
||||
hangingprotocols@0.0.1
|
||||
@ -54,7 +54,7 @@ hot-code-push@1.0.0
|
||||
html-tools@1.0.5
|
||||
htmljs@1.0.5
|
||||
http@1.1.1
|
||||
ian:accounts-ui-bootstrap-3@1.2.83
|
||||
ian:accounts-ui-bootstrap-3@1.2.89
|
||||
id-map@1.0.4
|
||||
insecure@1.0.4
|
||||
iron:controller@1.0.12
|
||||
@ -74,7 +74,7 @@ minifiers@1.1.7
|
||||
minimongo@1.0.10
|
||||
mobile-experience@1.0.1
|
||||
mobile-status-bar@1.0.6
|
||||
momentjs:moment@2.10.6
|
||||
momentjs:moment@2.11.2
|
||||
mongo@1.1.3
|
||||
mongo-id@1.0.1
|
||||
mrt:moment@2.8.1
|
||||
@ -82,8 +82,8 @@ npm-bcrypt@0.7.8_2
|
||||
npm-mongo@1.4.39_1
|
||||
observe-sequence@1.0.7
|
||||
ordered-dict@1.0.4
|
||||
practicalmeteor:chai@1.9.2_3
|
||||
practicalmeteor:loglevel@1.1.0_3
|
||||
practicalmeteor:chai@2.1.0_1
|
||||
practicalmeteor:loglevel@1.2.0_2
|
||||
promise@0.5.1
|
||||
random@1.0.5
|
||||
rate-limit@1.0.0
|
||||
@ -105,7 +105,7 @@ stylus@2.511.1
|
||||
templating@1.1.5
|
||||
templating-tools@1.0.0
|
||||
tracker@1.0.9
|
||||
twbs:bootstrap@3.3.5
|
||||
twbs:bootstrap@3.3.6
|
||||
ui@1.0.8
|
||||
underscore@1.0.4
|
||||
url@1.0.5
|
||||
@ -113,3 +113,4 @@ viewerbase@0.0.1
|
||||
webapp@1.2.3
|
||||
webapp-hashing@1.0.5
|
||||
worklist@0.0.1
|
||||
zuuk:stale-session@1.0.8
|
||||
|
||||
@ -12,8 +12,16 @@ if (Meteor.isClient){
|
||||
destination: '/worklist'
|
||||
},
|
||||
themeColors: {
|
||||
primary: 'black'
|
||||
primary: ""
|
||||
},
|
||||
passwordOptions: {
|
||||
showPasswordStrengthIndicator: false,
|
||||
requireRegexValidation: true,
|
||||
//requireStrongPasswords: false
|
||||
passwordHistoryCount: 6,
|
||||
failedAttemptsLimit: 5
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -30,11 +30,21 @@ var routerOptions = {
|
||||
};
|
||||
|
||||
Router.route('/', function() {
|
||||
this.render('worklist', routerOptions);
|
||||
// Check user is logged in
|
||||
if (!Meteor.user() || !Meteor.userId()) {
|
||||
this.render('entrySignIn', routerOptions);
|
||||
} else{
|
||||
this.render('worklist', routerOptions);
|
||||
}
|
||||
});
|
||||
|
||||
Router.route('/worklist', function() {
|
||||
this.render('worklist', routerOptions);
|
||||
// Check user is logged in
|
||||
if (!Meteor.user() || !Meteor.userId()) {
|
||||
this.render('entrySignIn', routerOptions);
|
||||
} else{
|
||||
this.render('worklist', routerOptions);
|
||||
}
|
||||
});
|
||||
|
||||
Router.route('/viewer/timepoints/:_id', {
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
## Contributing
|
||||
|
||||
**Submit your Pull Request on a Feature Branch**
|
||||
|
||||
- To ensure your pull-request has the greatest chance of getting merged in, please submit it on a feature branch rather than directly to master.
|
||||
- Please see [A successful Git branching model](http://nvie.com/posts/a-successful-git-branching-model/) for more details.
|
||||
|
||||
**Quality Assurance**
|
||||
- Pull Requests will be generally accepted as long as the QA tests pass on [Circle CI](https://circleci.com/gh/clinical-meteor/clinical-active-entry).
|
||||
- Begin a Pull Request by logging an Issue for discussion.
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
This package provides the SignIn, SignUp, and ForgotPassword pages.
|
||||
|
||||
[](https://circleci.com/gh/clinical-meteor/clinical-active-entry/tree/master)
|
||||
[](https://circleci.com/gh/clinical-meteor/active-entry/tree/master)
|
||||
|
||||
===============================
|
||||
#### Installation
|
||||
@ -16,7 +16,7 @@ meteor add clinical:active-entry
|
||||
|
||||
The following diagram represents the entry workflow that is being implemented in this package. This package is under active development, and is about half completed. Pull requests which help implement the following workflow will be fast-tracked and accepted into the package.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
@ -111,6 +111,9 @@ npm install -g starrynight
|
||||
# verification testing (a.k.a. package-level unit/integration testing)
|
||||
starrynight run-tests --type package-verification
|
||||
|
||||
#to run validation tests, you'll need an ``.initializeUsers()`` function
|
||||
meteor add clinical:accounts-housemd
|
||||
|
||||
#validation testing (a.k.a. application acceptance/end-to-end testing)
|
||||
starrynight autoscan
|
||||
starrynight run-tests --type validation
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
## Customize the test machine
|
||||
machine:
|
||||
node:
|
||||
version: 0.10.33
|
||||
|
||||
# Timezone
|
||||
timezone:
|
||||
@ -25,27 +27,48 @@ checkout:
|
||||
## Customize dependencies
|
||||
dependencies:
|
||||
cache_directories:
|
||||
- "~/.meteor" # relative to the user's home directory
|
||||
- ~/.meteor # relative to the user's home directory
|
||||
- ~/nvm/v0.10.33/lib/node_modules/starrynight
|
||||
- ~/nvm/v0.10.33/bin/starrynight
|
||||
|
||||
pre:
|
||||
# Install Starrynight unless it is cached
|
||||
- if [ ! -e ~/nvm/v0.10.33/bin/starrynight ]; then npm install -g starrynight; else echo "Starrynight seems to be cached"; fi;
|
||||
# Install Meteor
|
||||
- mkdir -p ${HOME}/.meteor
|
||||
# If Meteor is already cached, do not need to build it again.
|
||||
- if [ ! -e ${HOME}/.meteor/meteor ]; then curl https://install.meteor.com | /bin/sh; else echo "Meteor seems to be cached"; fi;
|
||||
# Link the meteor executable into /usr/bin
|
||||
- sudo ln -s $HOME/.meteor/meteor /usr/bin/meteor
|
||||
# Check if the helloworld directory already exists, if it doesn't, create the helloworld app
|
||||
# The following doesn't work, because it should be checking ${HOME}/active-entry/helloworld
|
||||
# - if [ ! -e ${HOME}/helloworld ]; then meteor create --release METEOR@1.1.0.3 helloworld; else echo "helloworld app seems to be cached"; fi;
|
||||
|
||||
override:
|
||||
- meteor || curl https://install.meteor.com | /bin/sh
|
||||
- npm install starrynight -g
|
||||
- meteor create --release METEOR@1.1.0.3 helloworld
|
||||
- cd helloworld
|
||||
- cd helloworld && ls -la
|
||||
- cd helloworld && rm helloworld.*
|
||||
- cd helloworld && meteor add anti:gagarin@0.4.11 accounts-base accounts-password session meteor-platform clinical:active-entry clinical:user-model
|
||||
- cd helloworld && git clone http://github.com/clinical-meteor/clinical-active-entry packages/active-entry
|
||||
- cd helloworld && starrynight autoconfig
|
||||
- cd ${HOME} && meteor create --release METEOR@1.1.0.3 helloworld
|
||||
- cd ${HOME}/helloworld
|
||||
- cd ${HOME}/helloworld && ls -la
|
||||
- cd ${HOME}/helloworld && rm helloworld.*
|
||||
- cd ${HOME}/helloworld && mkdir packages && mkdir packages/active-entry
|
||||
- cp -R * ${HOME}/helloworld/packages/active-entry
|
||||
- cd ${HOME}/helloworld && meteor add anti:gagarin@0.4.11 accounts-base accounts-password session meteor-platform clinical:user-model clinical:active-entry
|
||||
- cd ${HOME}/helloworld && starrynight autoconfig
|
||||
- cd ${HOME}/helloworld && meteor list
|
||||
- cat tests/gagarin/activeEntryTests.js
|
||||
- ls -la
|
||||
- cd ~ && ls -la
|
||||
- cd ${HOME} && pwd
|
||||
- cd ${HOME} && ls -la
|
||||
- cd ${HOME}/helloworld && ls -la
|
||||
|
||||
## Customize test commands
|
||||
test:
|
||||
pre:
|
||||
- cd helloworld && meteor:
|
||||
background: true
|
||||
- sleep 80
|
||||
- sleep 30
|
||||
override:
|
||||
- cd helloworld && starrynight run-tests --type package-verification
|
||||
- cd ${HOME}/helloworld && starrynight run-tests --type package-verification
|
||||
|
||||
## Customize deployment commands
|
||||
#deployment:
|
||||
|
||||
48
Packages/active-entry/components/changePassword/changePassword.html
Executable file
48
Packages/active-entry/components/changePassword/changePassword.html
Executable file
@ -0,0 +1,48 @@
|
||||
<template name="changePassword">
|
||||
<div id="changePassword" class="page entryPage" style="{{getOpacityWithCorner}}">
|
||||
<div class="content-scrollable">
|
||||
<div class="wrapper-auth">
|
||||
<div class="entryLogo" style="background-image: url('{{getLogoUrl}}')"></div>
|
||||
|
||||
<h1 id="changePasswordPageTitle" class="title-auth">Change Password</h1>
|
||||
<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>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="changePasswordPagePasswordInput" type="password" name="password" placeholder="Password" style="{{getPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Password"></span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="input-symbol">
|
||||
<input id="changePasswordPagePasswordConfirmInput" type="password" name="confirm" placeholder="Confirm Password" style="{{getConfirmPasswordStyling}}" />
|
||||
<span class="fa fa-lock" title="Confirm Password"></span>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button id="changePasswordButton" type="submit" class="btn-gray btn-main btn-large" style="{{getButtonColor}}">Change Password</button>
|
||||
<br><br>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
120
Packages/active-entry/components/changePassword/changePassword.js
Executable file
120
Packages/active-entry/components/changePassword/changePassword.js
Executable file
@ -0,0 +1,120 @@
|
||||
//==========================================
|
||||
|
||||
Router.route('/changePassword', {
|
||||
name: "changePassword",
|
||||
template: "changePassword"
|
||||
});
|
||||
|
||||
|
||||
Template.changePassword.helpers({
|
||||
getChangePasswordMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('changePasswordError')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;"
|
||||
} else {
|
||||
return "color: black;"
|
||||
}
|
||||
},
|
||||
getChangePasswordMessage: function (){
|
||||
if (ActiveEntry.errorMessages.get('changePasswordError')) {
|
||||
return ActiveEntry.errorMessages.get('changePasswordError');
|
||||
} else {
|
||||
return Session.get('defaultSignInMessage');
|
||||
}
|
||||
},
|
||||
getPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('password', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password is weak")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
},
|
||||
getConfirmPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('confirm', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords do not match")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Password is weak")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords match")) {
|
||||
return "border: 1px solid green";
|
||||
} 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();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
|
||||
ActiveEntry.verifyPassword(password);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
},
|
||||
'change, keyup #changePasswordPagePasswordConfirmInput': function (event, template) {
|
||||
var password = $('[name="password"]').val();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
},
|
||||
"submit": function (event, template) {
|
||||
event.preventDefault();
|
||||
|
||||
var oldPassword = $('[name="oldPassword"]').val();
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirmPassword = $('[name="confirm"]').val();
|
||||
|
||||
ActiveEntry.verifyConfirmPassword(password, confirmPassword);
|
||||
ActiveEntry.errorMessages.set('changePasswordError', null);
|
||||
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
5
Packages/active-entry/components/changePassword/changePassword.less
Executable file
5
Packages/active-entry/components/changePassword/changePassword.less
Executable file
@ -0,0 +1,5 @@
|
||||
#changePassword{
|
||||
input{
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -48,7 +48,7 @@ Template.entrySignIn.helpers({
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('email', "Email is poorly formatted")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('email', "Email present")) {
|
||||
} else if (ActiveEntry.successMessages.equals('email', "Email present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
@ -57,16 +57,16 @@ Template.entrySignIn.helpers({
|
||||
getPasswordValidationStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('password', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password is weak")) {
|
||||
} else if (ActiveEntry.errorMessages.equals('password', Session.get('passwordWarning'))) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password present")) {
|
||||
} else if (ActiveEntry.successMessages.equals('password', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
//==================================================================================================
|
||||
// COMPONENT OUTPUTS
|
||||
@ -77,10 +77,12 @@ Template.entrySignIn.events({
|
||||
},
|
||||
'click #forgotPasswordButton': function (event) {
|
||||
event.preventDefault();
|
||||
ActiveEntry.reset();
|
||||
Router.go('/forgotPassword');
|
||||
},
|
||||
"click #needAnAccountButton": function (event) {
|
||||
event.preventDefault();
|
||||
ActiveEntry.reset();
|
||||
Router.go('/entrySignUp');
|
||||
},
|
||||
'keyup input[name="email"]': function (event, template) {
|
||||
@ -88,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();
|
||||
@ -115,7 +121,7 @@ Template.entrySignIn.events({
|
||||
// ActiveEntry.signIn(emailValue, passwordValue);
|
||||
// },
|
||||
'click #signInToAppButton': function (event, template){
|
||||
console.log('click #signInToAppButton');
|
||||
ActiveEntry.reset();
|
||||
// var emailValue = template.$('[name=email]').val();
|
||||
// var passwordValue = template.$('[name=password]').val();
|
||||
var emailValue = template.$('#signInPageEmailInput').val();
|
||||
@ -123,9 +129,29 @@ Template.entrySignIn.events({
|
||||
|
||||
ActiveEntry.signIn(emailValue, passwordValue);
|
||||
event.preventDefault();
|
||||
},
|
||||
'keyup #entrySignIn': function(event, template) {
|
||||
if(event.keyCode == 13) {
|
||||
$("#signInToAppButton").click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
//==================================================================================================
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -10,10 +10,11 @@
|
||||
<h1 id="signUpPageTitle" class="title-auth">Join.</h1>
|
||||
<div id="signUpPageMessage" class="subtitle-auth" style="{{getSignUpMessageColor}}">{{getSignUpMessage}}</div>
|
||||
|
||||
<form>
|
||||
{{#if errorMessages}}
|
||||
<form id="entrySignUpForm">
|
||||
|
||||
{{#if entryErrorMessages}}
|
||||
<div id="errorMessages" class="list-errors">
|
||||
{{#each errorMessages}}
|
||||
{{#each entryErrorMessages}}
|
||||
<div class="alert alert-danger list-item">{{this}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -17,9 +17,9 @@ Router.route('/sign-up', {
|
||||
Template.entrySignUp.helpers({
|
||||
getSignUpMessageColor: function (){
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;"
|
||||
return "color: #a94442; background-color: #f2dede; border-color: #ebccd1;";
|
||||
} else {
|
||||
return "color: black;"
|
||||
return "color: black;";
|
||||
}
|
||||
},
|
||||
getSignUpMessage: function (){
|
||||
@ -29,6 +29,15 @@ Template.entrySignUp.helpers({
|
||||
return Session.get('defaultSignInMessage');
|
||||
}
|
||||
},
|
||||
entryErrorMessages: function () {
|
||||
var errorMessages = [];
|
||||
Object.keys(ActiveEntry.errorMessages.all()).forEach(function(key) {
|
||||
if (key !== "signInError" && ActiveEntry.errorMessages.get(key)) {
|
||||
errorMessages.push(ActiveEntry.errorMessages.get(key));
|
||||
}
|
||||
});
|
||||
return errorMessages;
|
||||
},
|
||||
getButtonText: function () {
|
||||
if (ActiveEntry.errorMessages.get('signInError')) {
|
||||
return ActiveEntry.errorMessages.get('signInError').message;
|
||||
@ -41,7 +50,7 @@ Template.entrySignUp.helpers({
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('email', "Email is poorly formatted")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('email', "Email present")) {
|
||||
} else if (ActiveEntry.successMessages.equals('email', "Email present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
@ -50,9 +59,9 @@ Template.entrySignUp.helpers({
|
||||
getPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('password', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password is weak")) {
|
||||
} else if (ActiveEntry.errorMessages.equals('password', Session.get('passwordWarning'))) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('password', "Password present")) {
|
||||
} else if (ActiveEntry.successMessages.equals('password', "Password present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
@ -61,9 +70,11 @@ Template.entrySignUp.helpers({
|
||||
getConfirmPasswordStyling: function () {
|
||||
if (ActiveEntry.errorMessages.equals('confirm', "Password is required")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords do not match")) {
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Password is weak")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('confirm', "Passwords match")) {
|
||||
} else if (ActiveEntry.successMessages.equals('confirm', "Passwords match")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
@ -74,7 +85,7 @@ Template.entrySignUp.helpers({
|
||||
return "border: 1px solid #a94442";
|
||||
} else if (ActiveEntry.errorMessages.equals('fullName', "Name is probably not complete")) {
|
||||
return "border: 1px solid #f2dede";
|
||||
} else if (ActiveEntry.errorMessages.equals('fullName', "Name present")) {
|
||||
} else if (ActiveEntry.successMessages.equals('fullName', "Name present")) {
|
||||
return "border: 1px solid green";
|
||||
} else {
|
||||
return "border: 1px solid gray";
|
||||
@ -85,6 +96,7 @@ Template.entrySignUp.helpers({
|
||||
Template.entrySignUp.events({
|
||||
"click #signUpPageSignInButton": function (event) {
|
||||
event.preventDefault();
|
||||
ActiveEntry.reset();
|
||||
Router.go('/entrySignIn');
|
||||
},
|
||||
'change, keyup #signUpPageEmailInput': function (event, template) {
|
||||
@ -115,25 +127,40 @@ Template.entrySignUp.events({
|
||||
ActiveEntry.verifyFullName(fullName);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
},
|
||||
// TODO: this is outdated, and should be changed to match the signature/pattern in entrySignIn
|
||||
'click #signUpPageJoinNowButton': function (event, template) {
|
||||
event.preventDefault();
|
||||
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
ActiveEntry.verifyPassword(event, template);
|
||||
|
||||
var newUser = {
|
||||
fullName: template.$('[name="fullName"]').val(),
|
||||
email: template.$('[name="email"]').val(),
|
||||
password: template.$('[name="password"]').val(),
|
||||
confirm: template.$('[name="confirm"]').val()
|
||||
};
|
||||
|
||||
ActiveEntry.signUp(
|
||||
newUser.email,
|
||||
newUser.password,
|
||||
newUser.confirm,
|
||||
newUser.fullName
|
||||
$('#signUpPageEmailInput').val(),
|
||||
$('#signUpPagePasswordInput').val(),
|
||||
$('#signUpPagePasswordConfirmInput').val(),
|
||||
$('#signUpPageFullNameInput').val()
|
||||
);
|
||||
},
|
||||
'keyup #entrySignUp': function(event, template) {
|
||||
if(event.keyCode == 13) {
|
||||
ActiveEntry.verifyFullName($("#signUpPageFullNameInput").val());
|
||||
ActiveEntry.verifyEmail($("#signUpPageEmailInput").val());
|
||||
ActiveEntry.verifyPassword($("#signUpPagePasswordInput").val());
|
||||
ActiveEntry.verifyConfirmPassword($("#signUpPagePasswordInput").val(), $("#signUpPagePasswordConfirmInput").val());
|
||||
|
||||
if (!ActiveEntry.errorMessages.get('signInError') &&
|
||||
ActiveEntry.successMessages.get('fullName') &&
|
||||
ActiveEntry.successMessages.get('email') &&
|
||||
ActiveEntry.successMessages.get('password') &&
|
||||
ActiveEntry.successMessages.get('confirm')) {
|
||||
$("#signUpPageJoinNowButton").click();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.entrySignUp.onRendered(function() {
|
||||
// Password strength meter for password inputs
|
||||
if (passwordValidationSettings.showPasswordStrengthIndicator) {
|
||||
this.$('#signUpPagePasswordInput').pwstrength(passwordValidationSettings.pwstrengthOptions);
|
||||
}
|
||||
|
||||
// Update password warning message if zxcvbn is active and zxcvbn function is defined
|
||||
if(passwordValidationSettings.requireStrongPasswords) {
|
||||
Session.set('passwordWarning', 'Password is weak');
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,3 +5,8 @@
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
#entrySignUp .progress{
|
||||
height: 5px;
|
||||
margin-bottom: 1px
|
||||
}
|
||||
@ -22,17 +22,31 @@ if (Meteor.isClient) {
|
||||
},
|
||||
themeColors: {
|
||||
primary: ""
|
||||
},
|
||||
passwordOptions: {
|
||||
showPasswordStrengthIndicator: true,
|
||||
requireRegexValidation: true,
|
||||
//requireStrongPasswords: false
|
||||
passwordHistoryCount: 6,
|
||||
failedAttemptsLimit: 5
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// requireRegexValidation toggles regex
|
||||
// reqiureStrongPasswords toggles zxcvbn
|
||||
|
||||
if (Meteor.isClient) {
|
||||
ActiveEntry.errorMessages = new ReactiveDict('errorMessages');
|
||||
ActiveEntry.errorMessages.set('signInError', false);
|
||||
|
||||
}
|
||||
// Success messages
|
||||
ActiveEntry.successMessages = new ReactiveDict('successMessages');
|
||||
|
||||
// Change password warning message according to whether zxcvbn is turned on
|
||||
Session.set('passwordWarning', 'Password must have at least 8 characters. It must contain at least 1 uppercase, 1 lowercase, 1 number and 1 special character.');
|
||||
}
|
||||
|
||||
ActiveEntry.configure = function (configObject) {
|
||||
if (Meteor.isClient) {
|
||||
@ -40,62 +54,101 @@ ActiveEntry.configure = function (configObject) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
ActiveEntry.verifyPassword = function (password) {
|
||||
if (password.length === 0) {
|
||||
ActiveEntry.errorMessages.set('password', 'Password is required');
|
||||
} else if (password.length < 8) {
|
||||
ActiveEntry.errorMessages.set('password', 'Password is weak');
|
||||
} else if (password.length >= 8) {
|
||||
ActiveEntry.errorMessages.set('password', 'Password present');
|
||||
ActiveEntry.successMessages.set('password', null);
|
||||
} else if (!checkPasswordStrength(password)) {
|
||||
ActiveEntry.errorMessages.set('password', Session.get('passwordWarning'));
|
||||
ActiveEntry.successMessages.set('password', null);
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('password', null);
|
||||
ActiveEntry.successMessages.set('password', 'Password present');
|
||||
}
|
||||
};
|
||||
ActiveEntry.verifyConfirmPassword = function (password, confirmPassword) {
|
||||
if (confirmPassword.length === 0) {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Password is required');
|
||||
} else if (confirmPassword.length < 8) {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Password is weak');
|
||||
}
|
||||
|
||||
if (confirmPassword === password) {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Passwords match');
|
||||
ActiveEntry.verifyConfirmPassword = function (password, confirmPassword) {
|
||||
// we have two different logic checks happening in this function
|
||||
// would be reasonable to separate them out into separate functions
|
||||
if (confirmPassword === "") {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Password is required');
|
||||
ActiveEntry.successMessages.set('confirm', null);
|
||||
} else if (confirmPassword === password) {
|
||||
ActiveEntry.errorMessages.set('confirm', null);
|
||||
ActiveEntry.successMessages.set('confirm', 'Passwords match');
|
||||
} else {
|
||||
ActiveEntry.errorMessages.set('confirm', 'Passwords do not match');
|
||||
ActiveEntry.successMessages.set('confirm', null);
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyEmail = function (email) {
|
||||
if (email.length === 0) {
|
||||
ActiveEntry.errorMessages.set('email', 'Email is required');
|
||||
ActiveEntry.successMessages.set('email', null);
|
||||
} else if (email.indexOf("@") === -1){
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEntry.verifyFullName = function (fullName) {
|
||||
if (fullName.length === 0) {
|
||||
ActiveEntry.errorMessages.set('fullName', 'Name is required');
|
||||
ActiveEntry.successMessages.set('fullName', null);
|
||||
} else if (fullName.indexOf(" ") === -1){
|
||||
ActiveEntry.errorMessages.set('fullName', 'Name is probably not complete');
|
||||
ActiveEntry.successMessages.set('fullName', null);
|
||||
} else if (fullName.indexOf(" ") >= 0){
|
||||
ActiveEntry.errorMessages.set('fullName', 'Name present');
|
||||
//ActiveEntry.errorMessages.set('fullName', 'Name present');
|
||||
ActiveEntry.errorMessages.set('fullName', null);
|
||||
ActiveEntry.successMessages.set('fullName', 'Name present');
|
||||
}
|
||||
};
|
||||
|
||||
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){
|
||||
@ -103,18 +156,36 @@ ActiveEntry.signUp = function (emailValue, passwordValue, confirmPassword, fullN
|
||||
ActiveEntry.verifyPassword(passwordValue);
|
||||
ActiveEntry.verifyConfirmPassword(passwordValue, confirmPassword);
|
||||
ActiveEntry.verifyFullName(fullName);
|
||||
ActiveEntry.errorMessages.set('signInError', null);
|
||||
|
||||
var errorIsFound = false;
|
||||
Object.keys(ActiveEntry.errorMessages.keys).forEach(function(key) {
|
||||
if (ActiveEntry.errorMessages.get(key) !== "null" && ActiveEntry.errorMessages.get(key) !== null) {
|
||||
errorIsFound = true;
|
||||
}
|
||||
});
|
||||
|
||||
if(errorIsFound) {
|
||||
return;
|
||||
}
|
||||
|
||||
Accounts.createUser({
|
||||
email: emailValue,
|
||||
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);
|
||||
}
|
||||
@ -134,17 +205,31 @@ 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();
|
||||
};
|
||||
|
||||
ActiveEntry.reset = function (){
|
||||
ActiveEntry.errorMessages.set('signInError', false);
|
||||
ActiveEntry.errorMessages.set('fullName', false);
|
||||
ActiveEntry.errorMessages.set('email', false);
|
||||
ActiveEntry.errorMessages.set('confirm', false);
|
||||
ActiveEntry.errorMessages.set('password', false);
|
||||
};
|
||||
|
||||
ActiveEntry.logoIsDisplayed = function (){
|
||||
var ActiveEntryConfig = Session.get('Photonic.ActiveEntry');
|
||||
return ActiveEntryConfig.logo.displayed;
|
||||
}
|
||||
};
|
||||
|
||||
59
Packages/active-entry/lib/checkPasswordStrength.js
Normal file
59
Packages/active-entry/lib/checkPasswordStrength.js
Normal file
@ -0,0 +1,59 @@
|
||||
passwordValidationSettings = {};
|
||||
|
||||
function getPasswordValidationSettings () {
|
||||
var ActiveEntryConfiguration = Session.get('Photonic.ActiveEntry');
|
||||
var validationSettings = {};
|
||||
|
||||
validationSettings.showPasswordStrengthIndicator = ActiveEntryConfiguration && ActiveEntryConfiguration.passwordOptions && ActiveEntryConfiguration.passwordOptions.showPasswordStrengthIndicator || false;
|
||||
validationSettings.requireRegexValidation = ActiveEntryConfiguration && ActiveEntryConfiguration.passwordOptions && ActiveEntryConfiguration.passwordOptions.requireRegexValidation || false;
|
||||
|
||||
if (validationSettings.showPasswordStrengthIndicator) {
|
||||
// Set password strength meter options
|
||||
validationSettings.pwstrengthOptions = {
|
||||
common: {
|
||||
minChar: 8
|
||||
},
|
||||
ui: {
|
||||
showVerdictsInsideProgressBar: true,
|
||||
showStatus: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Check if codetheweb:zxcvbn is defined
|
||||
if (typeof(zxcvbn) === typeof(Function)) {
|
||||
validationSettings.requireStrongPasswords = ActiveEntryConfiguration && ActiveEntryConfiguration.passwordOptions && ActiveEntryConfiguration.passwordOptions.requireStrongPasswords || false;
|
||||
// Set zxcvbn in pw strength meter
|
||||
if (validationSettings.showPasswordStrengthIndicator) {
|
||||
validationSettings.pwstrengthOptions.common.zxcvbn = passwordValidationSettings.requireStrongPasswords;
|
||||
}
|
||||
}
|
||||
|
||||
return validationSettings;
|
||||
}
|
||||
|
||||
Meteor.startup(function() {
|
||||
passwordValidationSettings = getPasswordValidationSettings();
|
||||
});
|
||||
|
||||
checkPasswordStrength = function(password) {
|
||||
if (passwordValidationSettings.requireStrongPasswords) {
|
||||
// Check zxcvbn
|
||||
var zxcvbnResult = zxcvbn(password);
|
||||
if (zxcvbnResult && zxcvbnResult.score > 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (passwordValidationSettings.requireRegexValidation) {
|
||||
// Apply validation rule
|
||||
var result = password.search(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*]{8,}$/i);
|
||||
if (result > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
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;
|
||||
};
|
||||
737
Packages/active-entry/lib/jquery.pwstrength.bootstrap.js
Normal file
737
Packages/active-entry/lib/jquery.pwstrength.bootstrap.js
Normal file
@ -0,0 +1,737 @@
|
||||
/*!
|
||||
* jQuery Password Strength plugin for Twitter Bootstrap
|
||||
*
|
||||
* Copyright (c) 2008-2013 Tane Piper
|
||||
* Copyright (c) 2013 Alejandro Blanco
|
||||
* Dual licensed under the MIT and GPL licenses.
|
||||
*/
|
||||
|
||||
(function (jQuery) {
|
||||
// Source: src/rules.js
|
||||
|
||||
|
||||
|
||||
var rulesEngine = {};
|
||||
|
||||
try {
|
||||
if (!jQuery && module && module.exports) {
|
||||
var jQuery = require("jquery"),
|
||||
jsdom = require("jsdom").jsdom;
|
||||
jQuery = jQuery(jsdom().parentWindow);
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
(function ($, rulesEngine) {
|
||||
"use strict";
|
||||
var validation = {};
|
||||
|
||||
rulesEngine.forbiddenSequences = [
|
||||
"0123456789", "abcdefghijklmnopqrstuvwxyz", "qwertyuiop", "asdfghjkl",
|
||||
"zxcvbnm", "!@#$%^&*()_+"
|
||||
];
|
||||
|
||||
validation.wordNotEmail = function (options, word, score) {
|
||||
if (word.match(
|
||||
/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i
|
||||
)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordLength = function (options, word, score) {
|
||||
var wordlen = word.length,
|
||||
lenScore = Math.pow(wordlen, options.rules.raisePower);
|
||||
if (wordlen < options.common.minChar) {
|
||||
lenScore = (lenScore + score);
|
||||
}
|
||||
return lenScore;
|
||||
};
|
||||
|
||||
validation.wordSimilarToUsername = function (options, word, score) {
|
||||
var username = $(options.common.usernameField).val();
|
||||
if (username && word.toLowerCase().match(username.replace(
|
||||
/[\-\[\]\/\{\}\(\)\*\+\=\?\:\.\\\^\$\|\!\,]/g, "\\$&").toLowerCase())) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordTwoCharacterClasses = function (options, word, score) {
|
||||
if (word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) ||
|
||||
(word.match(/([a-zA-Z])/) && word.match(/([0-9])/)) ||
|
||||
(word.match(/(.[!,@,#,$,%,\^,&,*,?,_,~])/) && word.match(/[a-zA-Z0-9_]/))) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordRepetitions = function (options, word, score) {
|
||||
if (word.match(/(.)\1\1/)) {
|
||||
return score;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordSequences = function (options, word, score) {
|
||||
var found = false,
|
||||
j;
|
||||
if (word.length > 2) {
|
||||
$.each(rulesEngine.forbiddenSequences, function (idx, seq) {
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
var sequences = [seq, seq.split('').reverse().join('')];
|
||||
$.each(sequences, function (idx, sequence) {
|
||||
for (j = 0; j < (word.length - 2); j += 1) { // iterate the word trough a sliding window of size 3:
|
||||
if (sequence.indexOf(word.toLowerCase().substring(j, j + 3)) > -1) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (found) {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
validation.wordLowercase = function (options, word, score) {
|
||||
return word.match(/[a-z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordUppercase = function (options, word, score) {
|
||||
return word.match(/[A-Z]/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneNumber = function (options, word, score) {
|
||||
return word.match(/\d+/) && score;
|
||||
};
|
||||
|
||||
validation.wordThreeNumbers = function (options, word, score) {
|
||||
return word.match(/(.*[0-9].*[0-9].*[0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordOneSpecialChar = function (options, word, score) {
|
||||
return word.match(/[!,@,#,$,%,\^,&,*,?,_,~]/) && score;
|
||||
};
|
||||
|
||||
validation.wordTwoSpecialChar = function (options, word, score) {
|
||||
return word.match(/(.*[!,@,#,$,%,\^,&,*,?,_,~].*[!,@,#,$,%,\^,&,*,?,_,~])/) && score;
|
||||
};
|
||||
|
||||
validation.wordUpperLowerCombo = function (options, word, score) {
|
||||
return word.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCombo = function (options, word, score) {
|
||||
return word.match(/([a-zA-Z])/) && word.match(/([0-9])/) && score;
|
||||
};
|
||||
|
||||
validation.wordLetterNumberCharCombo = function (options, word, score) {
|
||||
return word.match(
|
||||
/([a-zA-Z0-9].*[!,@,#,$,%,\^,&,*,?,_,~])|([!,@,#,$,%,\^,&,*,?,_,~].*[a-zA-Z0-9])/) &&
|
||||
score;
|
||||
};
|
||||
|
||||
rulesEngine.validation = validation;
|
||||
|
||||
rulesEngine.executeRules = function (options, word) {
|
||||
var totalScore = 0;
|
||||
|
||||
$.each(options.rules.activated, function (rule, active) {
|
||||
if (active) {
|
||||
var score = options.rules.scores[rule],
|
||||
funct = rulesEngine.validation[rule],
|
||||
result,
|
||||
errorMessage;
|
||||
|
||||
if (!$.isFunction(funct)) {
|
||||
funct = options.rules.extra[rule];
|
||||
}
|
||||
|
||||
if ($.isFunction(funct)) {
|
||||
result = funct(options, word, score);
|
||||
if (result) {
|
||||
totalScore += result;
|
||||
}
|
||||
if (result < 0 || (!$.isNumeric(result) && !result)) {
|
||||
errorMessage = options.ui.spanError(options, rule);
|
||||
if (errorMessage.length > 0) {
|
||||
options.instances.errors.push(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return totalScore;
|
||||
};
|
||||
}(jQuery, rulesEngine));
|
||||
|
||||
try {
|
||||
if (module && module.exports) {
|
||||
module.exports = rulesEngine;
|
||||
}
|
||||
} catch (ignore) {}
|
||||
|
||||
// Source: src/options.js
|
||||
|
||||
|
||||
|
||||
var defaultOptions = {};
|
||||
|
||||
defaultOptions.common = {};
|
||||
defaultOptions.common.minChar = 6;
|
||||
defaultOptions.common.usernameField = "#username";
|
||||
defaultOptions.common.userInputs = [
|
||||
// Selectors for input fields with user input
|
||||
];
|
||||
defaultOptions.common.onLoad = undefined;
|
||||
defaultOptions.common.onKeyUp = undefined;
|
||||
defaultOptions.common.zxcvbn = false;
|
||||
defaultOptions.common.zxcvbnTerms = [
|
||||
// List of disrecommended words
|
||||
];
|
||||
defaultOptions.common.debug = false;
|
||||
|
||||
defaultOptions.rules = {};
|
||||
defaultOptions.rules.extra = {};
|
||||
defaultOptions.rules.scores = {
|
||||
wordNotEmail: -100,
|
||||
wordLength: -50,
|
||||
wordSimilarToUsername: -100,
|
||||
wordSequences: -20,
|
||||
wordTwoCharacterClasses: 2,
|
||||
wordRepetitions: -25,
|
||||
wordLowercase: 1,
|
||||
wordUppercase: 3,
|
||||
wordOneNumber: 3,
|
||||
wordThreeNumbers: 5,
|
||||
wordOneSpecialChar: 3,
|
||||
wordTwoSpecialChar: 5,
|
||||
wordUpperLowerCombo: 2,
|
||||
wordLetterNumberCombo: 2,
|
||||
wordLetterNumberCharCombo: 2
|
||||
};
|
||||
defaultOptions.rules.activated = {
|
||||
wordNotEmail: true,
|
||||
wordLength: true,
|
||||
wordSimilarToUsername: true,
|
||||
wordSequences: true,
|
||||
wordTwoCharacterClasses: false,
|
||||
wordRepetitions: false,
|
||||
wordLowercase: true,
|
||||
wordUppercase: true,
|
||||
wordOneNumber: true,
|
||||
wordThreeNumbers: true,
|
||||
wordOneSpecialChar: true,
|
||||
wordTwoSpecialChar: true,
|
||||
wordUpperLowerCombo: true,
|
||||
wordLetterNumberCombo: true,
|
||||
wordLetterNumberCharCombo: true
|
||||
};
|
||||
defaultOptions.rules.raisePower = 1.4;
|
||||
|
||||
defaultOptions.ui = {};
|
||||
defaultOptions.ui.bootstrap2 = false;
|
||||
defaultOptions.ui.bootstrap4 = false;
|
||||
defaultOptions.ui.colorClasses = ["danger", "warning", "success"];
|
||||
defaultOptions.ui.showProgressBar = true;
|
||||
defaultOptions.ui.showPopover = false;
|
||||
defaultOptions.ui.popoverPlacement = "bottom";
|
||||
defaultOptions.ui.showStatus = false;
|
||||
defaultOptions.ui.spanError = function (options, key) {
|
||||
"use strict";
|
||||
var text = options.ui.errorMessages[key];
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return '<span style="color: #d52929">' + text + '</span>';
|
||||
};
|
||||
defaultOptions.ui.popoverError = function (errors) {
|
||||
"use strict";
|
||||
var message = "<div>Errors:<ul class='error-list' style='margin-bottom: 0;'>";
|
||||
|
||||
jQuery.each(errors, function (idx, err) {
|
||||
message += "<li>" + err + "</li>";
|
||||
});
|
||||
message += "</ul></div>";
|
||||
return message;
|
||||
};
|
||||
defaultOptions.ui.errorMessages = {
|
||||
wordLength: "Your password is too short",
|
||||
wordNotEmail: "Do not use your email as your password",
|
||||
wordSimilarToUsername: "Your password cannot contain your username",
|
||||
wordTwoCharacterClasses: "Use different character classes",
|
||||
wordRepetitions: "Too many repetitions",
|
||||
wordSequences: "Your password contains sequences"
|
||||
};
|
||||
defaultOptions.ui.verdicts = ["Weak", "Normal", "Medium", "Strong", "Very Strong"];
|
||||
defaultOptions.ui.showVerdicts = true;
|
||||
defaultOptions.ui.showVerdictsInsideProgressBar = false;
|
||||
defaultOptions.ui.useVerdictCssClass = false;
|
||||
defaultOptions.ui.showErrors = false;
|
||||
defaultOptions.ui.container = undefined;
|
||||
defaultOptions.ui.viewports = {
|
||||
progress: undefined,
|
||||
verdict: undefined,
|
||||
errors: undefined
|
||||
};
|
||||
defaultOptions.ui.scores = [14, 26, 38, 50];
|
||||
|
||||
// Source: src/ui.js
|
||||
|
||||
|
||||
|
||||
var ui = {};
|
||||
|
||||
(function ($, ui) {
|
||||
"use strict";
|
||||
|
||||
var statusClasses = ["error", "warning", "success"];
|
||||
|
||||
ui.getContainer = function (options, $el) {
|
||||
var $container;
|
||||
|
||||
$container = $(options.ui.container);
|
||||
if (!($container && $container.length === 1)) {
|
||||
$container = $el.parent();
|
||||
}
|
||||
return $container;
|
||||
};
|
||||
|
||||
ui.findElement = function ($container, viewport, cssSelector) {
|
||||
if (viewport) {
|
||||
return $container.find(viewport).find(cssSelector);
|
||||
}
|
||||
return $container.find(cssSelector);
|
||||
};
|
||||
|
||||
ui.getUIElements = function (options, $el) {
|
||||
var $container, selector, result;
|
||||
|
||||
if (options.instances.viewports) {
|
||||
return options.instances.viewports;
|
||||
}
|
||||
|
||||
$container = ui.getContainer(options, $el);
|
||||
|
||||
result = {};
|
||||
if (options.ui.bootstrap4) {
|
||||
selector = "progress.progress";
|
||||
} else {
|
||||
selector = "div.progress";
|
||||
}
|
||||
result.$progressbar = ui.findElement($container, options.ui.viewports.progress,
|
||||
selector);
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = result.$progressbar.find("span.password-verdict");
|
||||
}
|
||||
|
||||
if (!options.ui.showPopover) {
|
||||
if (!options.ui.showVerdictsInsideProgressBar) {
|
||||
result.$verdict = ui.findElement($container, options.ui.viewports.verdict,
|
||||
"span.password-verdict");
|
||||
}
|
||||
result.$errors = ui.findElement($container, options.ui.viewports.errors,
|
||||
"ul.error-list");
|
||||
}
|
||||
|
||||
options.instances.viewports = result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.initProgressBar = function (options, $el) {
|
||||
var $container = ui.getContainer(options, $el),
|
||||
progressbar = "<div class='progress'><div class='"; // Boostrap 2
|
||||
|
||||
if (!options.ui.bootstrap2 && !options.ui.bootstrap4) {
|
||||
// Bootstrap 3
|
||||
progressbar += "progress-";
|
||||
}
|
||||
progressbar += "bar'>";
|
||||
if (options.ui.bootstrap4) {
|
||||
// Boostrap 4
|
||||
progressbar = "<progress class='progress' value='0' max='100'>";
|
||||
}
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
progressbar += "<span class='password-verdict'></span>";
|
||||
}
|
||||
if (options.ui.bootstrap4) {
|
||||
progressbar += "</progress>";
|
||||
} else {
|
||||
progressbar += "</div></div>";
|
||||
}
|
||||
|
||||
if (options.ui.viewports.progress) {
|
||||
$container.find(options.ui.viewports.progress).append(progressbar);
|
||||
} else {
|
||||
$(progressbar).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initHelper = function (options, $el, html, viewport) {
|
||||
var $container = ui.getContainer(options, $el);
|
||||
if (viewport) {
|
||||
$container.find(viewport).append(html);
|
||||
} else {
|
||||
$(html).insertAfter($el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.initVerdict = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<span class='password-verdict'></span>",
|
||||
options.ui.viewports.verdict);
|
||||
};
|
||||
|
||||
ui.initErrorList = function (options, $el) {
|
||||
ui.initHelper(options, $el, "<ul class='error-list'></ul>",
|
||||
options.ui.viewports.errors);
|
||||
};
|
||||
|
||||
ui.initPopover = function (options, $el) {
|
||||
$el.popover("destroy");
|
||||
$el.popover({
|
||||
html: true,
|
||||
placement: options.ui.popoverPlacement,
|
||||
trigger: "manual",
|
||||
content: " "
|
||||
});
|
||||
};
|
||||
|
||||
ui.initUI = function (options, $el) {
|
||||
if (options.ui.showPopover) {
|
||||
ui.initPopover(options, $el);
|
||||
} else {
|
||||
if (options.ui.showErrors) {
|
||||
ui.initErrorList(options, $el);
|
||||
}
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.initVerdict(options, $el);
|
||||
}
|
||||
}
|
||||
if (options.ui.showProgressBar) {
|
||||
ui.initProgressBar(options, $el);
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateProgressBar = function (options, $el, cssClass, percentage) {
|
||||
var $progressbar = ui.getUIElements(options, $el).$progressbar,
|
||||
$bar = $progressbar.find(".progress-bar"),
|
||||
cssPrefix = "progress-";
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
$bar = $progressbar.find(".bar");
|
||||
cssPrefix = "";
|
||||
}
|
||||
|
||||
$.each(options.ui.colorClasses, function (idx, value) {
|
||||
if (options.ui.bootstrap4) {
|
||||
$progressbar.removeClass(cssPrefix + value);
|
||||
} else {
|
||||
$bar.removeClass(cssPrefix + "bar-" + value);
|
||||
}
|
||||
});
|
||||
if (options.ui.bootstrap4) {
|
||||
$progressbar.addClass(cssPrefix + options.ui.colorClasses[cssClass]);
|
||||
$progressbar.val(percentage);
|
||||
} else {
|
||||
$bar.addClass(cssPrefix + "bar-" + options.ui.colorClasses[cssClass]);
|
||||
$bar.css("width", percentage + '%');
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateVerdict = function (options, $el, cssClass, text) {
|
||||
var $verdict = ui.getUIElements(options, $el).$verdict;
|
||||
$verdict.removeClass(options.ui.colorClasses.join(' '));
|
||||
if (cssClass > -1) {
|
||||
$verdict.addClass(options.ui.colorClasses[cssClass]);
|
||||
}
|
||||
$verdict.html(text);
|
||||
};
|
||||
|
||||
ui.updateErrors = function (options, $el) {
|
||||
var $errors = ui.getUIElements(options, $el).$errors,
|
||||
html = "";
|
||||
$.each(options.instances.errors, function (idx, err) {
|
||||
html += "<li>" + err + "</li>";
|
||||
});
|
||||
$errors.html(html);
|
||||
};
|
||||
|
||||
ui.updatePopover = function (options, $el, verdictText) {
|
||||
var popover = $el.data("bs.popover"),
|
||||
html = "",
|
||||
hide = true;
|
||||
|
||||
if (options.ui.showVerdicts &&
|
||||
!options.ui.showVerdictsInsideProgressBar &&
|
||||
verdictText.length > 0) {
|
||||
html = "<h5><span class='password-verdict'>" + verdictText +
|
||||
"</span></h5>";
|
||||
hide = false;
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
if (options.instances.errors.length > 0) {
|
||||
hide = false;
|
||||
}
|
||||
html += options.ui.popoverError(options.instances.errors);
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
$el.popover("hide");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.ui.bootstrap2) {
|
||||
popover = $el.data("popover");
|
||||
}
|
||||
|
||||
if (popover.$arrow && popover.$arrow.parents("body").length > 0) {
|
||||
$el.find("+ .popover .popover-content").html(html);
|
||||
} else {
|
||||
// It's hidden
|
||||
popover.options.content = html;
|
||||
$el.popover("show");
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateFieldStatus = function (options, $el, cssClass) {
|
||||
var targetClass = options.ui.bootstrap2 ? ".control-group" : ".form-group",
|
||||
$container = $el.parents(targetClass).first();
|
||||
|
||||
$.each(statusClasses, function (idx, css) {
|
||||
if (!options.ui.bootstrap2) {
|
||||
css = "has-" + css;
|
||||
}
|
||||
$container.removeClass(css);
|
||||
});
|
||||
|
||||
cssClass = statusClasses[cssClass];
|
||||
if (!options.ui.bootstrap2) {
|
||||
cssClass = "has-" + cssClass;
|
||||
}
|
||||
$container.addClass(cssClass);
|
||||
};
|
||||
|
||||
ui.percentage = function (score, maximun) {
|
||||
var result = Math.floor(100 * score / maximun);
|
||||
result = result <= 0 ? 1 : result; // Don't show the progress bar empty
|
||||
result = result > 100 ? 100 : result;
|
||||
return result;
|
||||
};
|
||||
|
||||
ui.getVerdictAndCssClass = function (options, score) {
|
||||
var cssClass, verdictText, level;
|
||||
|
||||
if (score <= 0) {
|
||||
cssClass = 0;
|
||||
level = -1;
|
||||
verdictText = options.ui.verdicts[0];
|
||||
} else if (score < options.ui.scores[0]) {
|
||||
cssClass = 0;
|
||||
level = 0;
|
||||
verdictText = options.ui.verdicts[0];
|
||||
} else if (score < options.ui.scores[1]) {
|
||||
cssClass = 0;
|
||||
level = 1;
|
||||
verdictText = options.ui.verdicts[1];
|
||||
} else if (score < options.ui.scores[2]) {
|
||||
cssClass = 1;
|
||||
level = 2;
|
||||
verdictText = options.ui.verdicts[2];
|
||||
} else if (score < options.ui.scores[3]) {
|
||||
cssClass = 1;
|
||||
level = 3;
|
||||
verdictText = options.ui.verdicts[3];
|
||||
} else {
|
||||
cssClass = 2;
|
||||
level = 4;
|
||||
verdictText = options.ui.verdicts[4];
|
||||
}
|
||||
|
||||
return [verdictText, cssClass, level];
|
||||
};
|
||||
|
||||
ui.updateUI = function (options, $el, score) {
|
||||
var cssClass, barPercentage, verdictText, verdictCssClass;
|
||||
|
||||
cssClass = ui.getVerdictAndCssClass(options, score);
|
||||
verdictText = score === 0 ? '' : cssClass[0];
|
||||
cssClass = cssClass[1];
|
||||
verdictCssClass = options.ui.useVerdictCssClass ? cssClass : -1;
|
||||
|
||||
if (options.ui.showProgressBar) {
|
||||
barPercentage = ui.percentage(score, options.ui.scores[3]);
|
||||
ui.updateProgressBar(options, $el, cssClass, barPercentage);
|
||||
if (options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ui.showStatus) {
|
||||
ui.updateFieldStatus(options, $el, cssClass);
|
||||
}
|
||||
|
||||
if (options.ui.showPopover) {
|
||||
ui.updatePopover(options, $el, verdictText);
|
||||
} else {
|
||||
if (options.ui.showVerdicts && !options.ui.showVerdictsInsideProgressBar) {
|
||||
ui.updateVerdict(options, $el, verdictCssClass, verdictText);
|
||||
}
|
||||
if (options.ui.showErrors) {
|
||||
ui.updateErrors(options, $el);
|
||||
}
|
||||
}
|
||||
};
|
||||
}(jQuery, ui));
|
||||
|
||||
// Source: src/methods.js
|
||||
|
||||
|
||||
|
||||
var methods = {};
|
||||
|
||||
(function ($, methods) {
|
||||
"use strict";
|
||||
var onKeyUp, applyToAll;
|
||||
|
||||
onKeyUp = function (event) {
|
||||
var $el = $(event.target),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
word = $el.val(),
|
||||
userInputs,
|
||||
verdictText,
|
||||
verdictLevel,
|
||||
score;
|
||||
|
||||
if (options === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.instances.errors = [];
|
||||
if (word.length === 0) {
|
||||
score = 0;
|
||||
} else {
|
||||
if (options.common.zxcvbn) {
|
||||
userInputs = [];
|
||||
$.each(options.common.userInputs.concat([options.common.usernameField]), function (
|
||||
idx, selector) {
|
||||
var value = $(selector).val();
|
||||
if (value) {
|
||||
userInputs.push(value);
|
||||
}
|
||||
});
|
||||
userInputs = userInputs.concat(options.common.zxcvbnTerms);
|
||||
score = Math.log2(zxcvbn(word, userInputs).guesses);
|
||||
} else {
|
||||
score = rulesEngine.executeRules(options, word);
|
||||
}
|
||||
}
|
||||
ui.updateUI(options, $el, score);
|
||||
verdictText = ui.getVerdictAndCssClass(options, score);
|
||||
verdictLevel = verdictText[2];
|
||||
verdictText = verdictText[0];
|
||||
|
||||
if (options.common.debug) {
|
||||
console.log(score + ' - ' + verdictText);
|
||||
}
|
||||
|
||||
if ($.isFunction(options.common.onKeyUp)) {
|
||||
options.common.onKeyUp(event, {
|
||||
score: score,
|
||||
verdictText: verdictText,
|
||||
verdictLevel: verdictLevel
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
methods.init = function (settings) {
|
||||
this.each(function (idx, el) {
|
||||
// Make it deep extend (first param) so it extends too the
|
||||
// rules and other inside objects
|
||||
var clonedDefaults = $.extend(true, {}, defaultOptions),
|
||||
localOptions = $.extend(true, clonedDefaults, settings),
|
||||
$el = $(el);
|
||||
|
||||
localOptions.instances = {};
|
||||
$el.data("pwstrength-bootstrap", localOptions);
|
||||
$el.on("keyup", onKeyUp);
|
||||
$el.on("change", onKeyUp);
|
||||
$el.on("paste", onKeyUp);
|
||||
|
||||
ui.initUI(localOptions, $el);
|
||||
if ($.trim($el.val())) { // Not empty, calculate the strength
|
||||
$el.trigger("keyup");
|
||||
}
|
||||
|
||||
if ($.isFunction(localOptions.common.onLoad)) {
|
||||
localOptions.common.onLoad();
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
methods.destroy = function () {
|
||||
this.each(function (idx, el) {
|
||||
var $el = $(el),
|
||||
options = $el.data("pwstrength-bootstrap"),
|
||||
elements = ui.getUIElements(options, $el);
|
||||
elements.$progressbar.remove();
|
||||
elements.$verdict.remove();
|
||||
elements.$errors.remove();
|
||||
$el.removeData("pwstrength-bootstrap");
|
||||
});
|
||||
};
|
||||
|
||||
methods.forceUpdate = function () {
|
||||
this.each(function (idx, el) {
|
||||
var event = {
|
||||
target: el
|
||||
};
|
||||
onKeyUp(event);
|
||||
});
|
||||
};
|
||||
|
||||
methods.addRule = function (name, method, score, active) {
|
||||
this.each(function (idx, el) {
|
||||
var options = $(el).data("pwstrength-bootstrap");
|
||||
|
||||
options.rules.activated[name] = active;
|
||||
options.rules.scores[name] = score;
|
||||
options.rules.extra[name] = method;
|
||||
});
|
||||
};
|
||||
|
||||
applyToAll = function (rule, prop, value) {
|
||||
this.each(function (idx, el) {
|
||||
$(el).data("pwstrength-bootstrap").rules[prop][rule] = value;
|
||||
});
|
||||
};
|
||||
|
||||
methods.changeScore = function (rule, score) {
|
||||
applyToAll.call(this, rule, "scores", score);
|
||||
};
|
||||
|
||||
methods.ruleActive = function (rule, active) {
|
||||
applyToAll.call(this, rule, "activated", active);
|
||||
};
|
||||
|
||||
$.fn.pwstrength = function (method) {
|
||||
var result;
|
||||
|
||||
if (methods[method]) {
|
||||
result = methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
} else if (typeof method === "object" || !method) {
|
||||
result = methods.init.apply(this, arguments);
|
||||
} else {
|
||||
$.error("Method " + method + " does not exist on jQuery.pwstrength-bootstrap");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}(jQuery, methods));
|
||||
}(jQuery));
|
||||
@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
name: 'clinical:active-entry',
|
||||
version: '1.5.14',
|
||||
version: '1.5.16',
|
||||
summary: 'SignIn, SignUp, and ForgotPassword pages for Clinical Framework.',
|
||||
git: 'https://github.com/clinical-meteor/clinical-active-entry',
|
||||
documentation: 'README.md'
|
||||
@ -16,20 +16,29 @@ Package.onUse(function (api) {
|
||||
'grove:less@0.1.1',
|
||||
'session',
|
||||
'reactive-dict',
|
||||
'accounts-base',
|
||||
'accounts-password'
|
||||
//'codetheweb:zxcvbn'
|
||||
], ['client']);
|
||||
|
||||
api.use([
|
||||
'accounts-base',
|
||||
'accounts-password'
|
||||
], ['server']);
|
||||
]);
|
||||
|
||||
api.use([
|
||||
'zuuk:stale-session@1.0.8'
|
||||
], ['client', 'server']);
|
||||
|
||||
api.addFiles([
|
||||
'lib/ActiveEntry.js',
|
||||
'lib/Accounts.js'
|
||||
]);
|
||||
|
||||
api.addFiles([
|
||||
'lib/jquery.pwstrength.bootstrap.js',
|
||||
'lib/checkPasswordStrength.js',
|
||||
'lib/hashCodeGenerator.js'
|
||||
], ['client']);
|
||||
|
||||
api.imply('accounts-base');
|
||||
api.imply('accounts-password');
|
||||
|
||||
@ -49,6 +58,10 @@ Package.onUse(function (api) {
|
||||
'components/forgotPassword/forgotPassword.js',
|
||||
'components/forgotPassword/forgotPassword.less',
|
||||
|
||||
'components/changePassword/changePassword.html',
|
||||
'components/changePassword/changePassword.js',
|
||||
'components/changePassword/changePassword.less',
|
||||
|
||||
], ['client']);
|
||||
|
||||
|
||||
|
||||
@ -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()}});
|
||||
}
|
||||
});
|
||||
|
||||
@ -43,7 +43,7 @@ describe('clinical:active-entry', function () {
|
||||
it('Email validation confirms it is a properly formatted email.', function () {
|
||||
return client.execute(function (a) {
|
||||
ActiveEntry.verifyEmail('janedoe@somewhere.com');
|
||||
expect(ActiveEntry.errorMessages.get('email')).to.equal("Email present");
|
||||
expect(ActiveEntry.successMessages.get('email')).to.equal("Email present");
|
||||
|
||||
ActiveEntry.verifyEmail('');
|
||||
expect(ActiveEntry.errorMessages.get('email')).to.equal("Email is required");
|
||||
@ -61,24 +61,28 @@ describe('clinical:active-entry', function () {
|
||||
expect(ActiveEntry.errorMessages.get('password')).to.equal("Password is required");
|
||||
|
||||
ActiveEntry.verifyPassword('kittens');
|
||||
expect(ActiveEntry.errorMessages.get('password')).to.equal("Password is weak");
|
||||
expect(ActiveEntry.errorMessages.get('password')).to.equal(Session.get('passwordWarning'));
|
||||
|
||||
ActiveEntry.verifyPassword('kittens123');
|
||||
expect(ActiveEntry.errorMessages.get('password')).to.equal("Password present");
|
||||
ActiveEntry.verifyPassword('K1tt#ns123');
|
||||
expect(ActiveEntry.successMessages.get('password')).to.equal("Password present");
|
||||
});
|
||||
});
|
||||
|
||||
// ActiveEntry.verifyConfirmPassword
|
||||
it('Password match validation confirms that two passwords are the same.', function () {
|
||||
it('Password match confirms that two passwords are the same.', function () {
|
||||
return client.execute(function (a) {
|
||||
ActiveEntry.verifyConfirmPassword('kittens123', '');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Password is required");
|
||||
ActiveEntry.verifyConfirmPassword('K1tt#kittens', 'kittens');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Passwords do not match");
|
||||
|
||||
ActiveEntry.verifyConfirmPassword('kittens123', 'kittens');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Password is weak");
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Passwords do not match");
|
||||
|
||||
ActiveEntry.verifyConfirmPassword('kittens123', 'kittens123');
|
||||
expect(ActiveEntry.errorMessages.get('confirm')).to.equal("Passwords match");
|
||||
|
||||
ActiveEntry.verifyConfirmPassword('K1tt#ns123', 'K1tt#ns123');
|
||||
expect(ActiveEntry.successMessages.get('confirm')).to.equal("Passwords match");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@ -92,18 +96,33 @@ describe('clinical:active-entry', function () {
|
||||
expect(ActiveEntry.errorMessages.get('fullName')).to.equal("Name is probably not complete");
|
||||
|
||||
ActiveEntry.verifyFullName('Jane Doe');
|
||||
expect(ActiveEntry.errorMessages.get('fullName')).to.equal("Name present");
|
||||
expect(ActiveEntry.successMessages.get('fullName')).to.equal("Name present");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// // ActiveEntry.signIn
|
||||
// it('Newly created user record should have role, profile, and name set.', function () {
|
||||
// return client.execute(function () {
|
||||
// ActiveEntry.signUp('janedoe@test.org', 'Janed*e123', 'Janed*e123', 'Jane Doe');
|
||||
// expect(ActiveEntry.successMessages.get('fullName')).to.equal("Name present");
|
||||
// }).then(function (){
|
||||
// return server.wait(500, 'until account is created on the server', function () {
|
||||
// return Meteor.users.findOne({'emails.address': 'janedoe@test.org'});
|
||||
// }).then(function (user){
|
||||
// expect(user.role).to.equal('user');
|
||||
// expect(user.profile.fullName).to.equal('Jane Doe');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// ActiveEntry.signIn
|
||||
it('Newly created user record should have role, profile, and name set.', function () {
|
||||
return client.execute(function () {
|
||||
ActiveEntry.signUp('janedoe@test.org', 'janedoe123', 'janedoe123', 'Jane Doe');
|
||||
expect(ActiveEntry.errorMessages.get('fullName')).to.equal("Name present");
|
||||
// ActiveEntry.signUp('janedoe@test.org', 'Janed*e123', 'Janed*e123', 'Jane Doe');
|
||||
ActiveEntry.signUp('janedoe@test.org', 'Janedoe123', 'Janedoe123', 'Jane Doe');
|
||||
expect(ActiveEntry.successMessages.get('fullName')).to.equal("Name present");
|
||||
}).then(function (){
|
||||
return server.wait(300, 'until account is created on the server', function () {
|
||||
return server.wait(500, 'until account is created on the server', function () {
|
||||
return Meteor.users.findOne({'emails.address': 'janedoe@test.org'});
|
||||
}).then(function (user){
|
||||
expect(user.role).to.equal('user');
|
||||
@ -113,7 +132,6 @@ describe('clinical:active-entry', function () {
|
||||
});
|
||||
|
||||
|
||||
|
||||
it("Newly created user should have fullName(), preferredName(), and familyName() methods.", function () {
|
||||
return server.execute(function () {
|
||||
var user = Meteor.users.findOne({'emails.address': 'janedoe@test.org'});
|
||||
@ -121,12 +139,12 @@ describe('clinical:active-entry', function () {
|
||||
expect(user.fullName()).to.equal('Jane Doe');
|
||||
expect(user.givenName()).to.equal('Jane');
|
||||
expect(user.familyName()).to.equal('Doe');
|
||||
}).then(function(){
|
||||
}).then(function (){
|
||||
// client.wait(500, "until user is logged out", function(){
|
||||
// Meteor.logout();
|
||||
// });
|
||||
return client.promise(function (resolve){
|
||||
Meteor.logout(function(error, result){
|
||||
Meteor.logout(function (error, result){
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@ -136,7 +154,7 @@ describe('clinical:active-entry', function () {
|
||||
it("Newly created user can sign in to the application.", function () {
|
||||
return client.execute(function () {
|
||||
expect(Meteor.userId()).to.not.exist;
|
||||
ActiveEntry.signIn('janedoe@test.org', 'janedoe123');
|
||||
ActiveEntry.signIn('janedoe@test.org', 'Janed*e123');
|
||||
}).then(function (){
|
||||
client.wait(3000, "for user to sign in", function (){
|
||||
expect(Meteor.userId()).to.exist;
|
||||
@ -146,7 +164,7 @@ describe('clinical:active-entry', function () {
|
||||
it("Newly created user can sign out of the application.", function () {
|
||||
return client.execute(function () {
|
||||
expect(Meteor.userId()).to.not.exist;
|
||||
ActiveEntry.signIn('janedoe@test.org', 'janedoe123');
|
||||
ActiveEntry.signIn('janedoe@test.org', 'Janed*e123');
|
||||
}).then(function (){
|
||||
client.wait(3000, "for user to sign in", function (){
|
||||
expect(Meteor.userId()).to.exist;
|
||||
|
||||
@ -15,19 +15,20 @@
|
||||
// existing user should be able to sign in on desktop
|
||||
// existing user should be able to sign in on tablet
|
||||
// existing user should be able to sign in on phone
|
||||
// existing user should be able to change their password
|
||||
// company logo should display on sign//in page
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
tags: ['users', 'entry'],
|
||||
before: function(client){
|
||||
before: function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignUp")
|
||||
.initializeUsers()
|
||||
.resizeWindow(1024, 768)
|
||||
.resizeWindow(1600, 1200);
|
||||
},
|
||||
"new user should be able to register on desktop" : function (client) {
|
||||
"new user should be able to register on desktop": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
.verify.elementPresent("#signUpPageTitle")
|
||||
@ -35,38 +36,38 @@ module.exports = {
|
||||
.verify.elementPresent("#signUpPageEmailInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPageJoinNowButton")
|
||||
.verify.elementPresent("#signUpPageSignInButton")
|
||||
.verify.elementPresent("#signUpPageSignInButton");
|
||||
},
|
||||
"company logo should display on sign-in page" : function (client) {
|
||||
"company logo should display on sign-in page": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
.verify.elementPresent("#entryAppLogo")
|
||||
.verify.elementPresent("#entryAppLogo");
|
||||
},
|
||||
"user should be able to request be able to create new account" : function (client) {
|
||||
"user should be able to request be able to create new account": function (client) {
|
||||
client.verify.elementPresent("#signUpPageEmailInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.elementPresent("#signUpPageJoinNowButton")
|
||||
.verify.elementPresent("#signUpPageJoinNowButton");
|
||||
},
|
||||
|
||||
"guest should be notified if password is insecure" : function (client) {
|
||||
"guest should be notified if password is insecure": function (client) {
|
||||
client
|
||||
.clearValue("input")
|
||||
.verify.elementPresent("#signUpPagePasswordInput")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordInput", "jan")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.setValue("#signUpPagePasswordInput", "icedoe123")
|
||||
.setValue("#signUpPagePasswordInput", "iceD*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid green')
|
||||
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordConfirmInput", "ja")
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border',
|
||||
'1px solid rgb(242, 222, 222)')
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.setValue("#signUpPagePasswordConfirmInput", "janicedoe123")
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid green')
|
||||
.setValue("#signUpPagePasswordConfirmInput", "Janiced*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid green');
|
||||
},
|
||||
"guest should be notified if passwords do not match" : function (client) {
|
||||
"guest should be notified if passwords do not match": function (client) {
|
||||
client
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.clearValue("#signUpPagePasswordInput")
|
||||
@ -74,14 +75,14 @@ module.exports = {
|
||||
.pause(500)
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid gray')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordInput", "janicedoe123")
|
||||
.setValue("#signUpPagePasswordInput", "Janiced*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#signUpPagePasswordConfirmInput", "janicedoe123")
|
||||
.setValue("#signUpPagePasswordConfirmInput", "Janiced*e123")
|
||||
.verify.cssProperty('#signUpPagePasswordInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#signUpPagePasswordConfirmInput', 'border', '1px solid green');
|
||||
},
|
||||
"guest should be notified if email is not correctly formatted" : function (client) {
|
||||
"guest should be notified if email is not correctly formatted": function (client) {
|
||||
client
|
||||
.clearValue("#signUpPageEmailInput")
|
||||
.resetEntry()
|
||||
@ -90,72 +91,109 @@ module.exports = {
|
||||
.setValue("#signUpPageEmailInput", "janicedoe")
|
||||
.verify.cssProperty('#signUpPageEmailInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.setValue("#signUpPageEmailInput", "@symptomatic.io")
|
||||
.verify.cssProperty('#signUpPageEmailInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#signUpPageEmailInput', 'border', '1px solid green');
|
||||
},
|
||||
"when new user fills out form and registers, new user should get created" : function (client) {
|
||||
"when new user fills out form and registers, new user should get created": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#entrySignUp")
|
||||
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.clearValue("#signUpPagePasswordConfirmInput")
|
||||
.clearValue("#signUpPagePasswordInput")
|
||||
.clearValue("#signUpPageFullNameInput")
|
||||
.clearValue("#signUpPageEmailInput")
|
||||
.resetEntry()
|
||||
|
||||
.setValue("#signUpPageFullNameInput", "Janice Doe")
|
||||
.setValue("#signUpPageFullNameInput", "Janice Doe")
|
||||
.setValue("#signUpPageEmailInput", "janicedoe@symptomatic.io")
|
||||
.setValue("#signUpPagePasswordInput", "janicedoe123")
|
||||
.setValue("#signUpPagePasswordConfirmInput", "janicedoe123")
|
||||
.setValue("#signUpPagePasswordInput", "Janiced*e123")
|
||||
.setValue("#signUpPagePasswordConfirmInput", "Janiced*e123")
|
||||
|
||||
.click("#signUpPageJoinNowButton").pause(1000)
|
||||
.click("#signUpPageJoinNowButton").pause(1000)
|
||||
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io");
|
||||
},
|
||||
"user should be able to signout" : function (client) {
|
||||
"user should be able to signout": function (client) {
|
||||
client
|
||||
.verify.elementPresent("#logoutButton")
|
||||
.click("#logoutButton").pause(300)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"user should be able to request reset password email" : function (client) {
|
||||
client
|
||||
"user should be able to request reset password email": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.verify.elementPresent("#forgotPasswordButton")
|
||||
.click("#forgotPasswordButton")
|
||||
.verify.elementPresent("#forgotPassword")
|
||||
.verify.elementPresent("#signInPageEmailInput")
|
||||
.verify.elementPresent("#sendReminderButton")
|
||||
.verify.elementPresent("#sendReminderButton");
|
||||
},
|
||||
"existing user should be able to sign in on desktop" : function (client) {
|
||||
"existing user should be able to sign in on desktop": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1600, 1200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"existing user should be able to sign in on tablet": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1024, 768)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"existing user should be able to sign in on phone": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(320, 960)
|
||||
// .verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.click("#navbarHeader").pause(300)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In");
|
||||
},
|
||||
"existing user should be able to change their password" : function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1600, 1200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "janicedoe123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.url("http://localhost:3000/changePassword")
|
||||
.verify.elementPresent("#changePasswordPageOldPasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordConfirmInput")
|
||||
.verify.elementPresent("#changePasswordButton")
|
||||
},
|
||||
"existing user should be able to sign in on tablet" : function (client) {
|
||||
"existing user should be notified if desired new password is insecure" : function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(1024, 768)
|
||||
.resizeWindow(1600, 1200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "janicedoe123").pause(500)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
},
|
||||
"existing user should be able to sign in on phone" : function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignIn")
|
||||
.resizeWindow(320, 960)
|
||||
// .verify.containsText("#usernameLink", "Sign In")
|
||||
.signIn("janicedoe@symptomatic.io", "janicedoe123").pause(500)
|
||||
.click("#sidebarToggle").pause(300)
|
||||
.verify.containsText("#usernameLink", "janicedoe@symptomatic.io")
|
||||
.click("#logoutButton").pause(200)
|
||||
.verify.containsText("#usernameLink", "Sign In")
|
||||
.url("http://localhost:3000/changePassword")
|
||||
.verify.elementPresent("#changePasswordPageOldPasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordInput")
|
||||
.verify.elementPresent("#changePasswordPagePasswordConfirmInput")
|
||||
.verify.elementPresent("#changePasswordButton")
|
||||
.verify.cssProperty('#changePasswordPagePasswordInput', 'border', '1px solid gray')
|
||||
.setValue("#changePasswordPagePasswordInput", "jan")
|
||||
.verify.cssProperty('#changePasswordPagePasswordInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.setValue("#changePasswordPagePasswordInput", "icedoe123")
|
||||
.verify.cssProperty('#changePasswordPagePasswordInput', 'border', '1px solid green')
|
||||
.verify.cssProperty('#changePasswordPagePasswordConfirmInput', 'border', '1px solid gray')
|
||||
.setValue("#changePasswordPagePasswordConfirmInput", "ja")
|
||||
.verify.cssProperty('#changePasswordPagePasswordConfirmInput', 'border', '1px solid rgb(242, 222, 222)')
|
||||
.clearValue("#changePasswordPagePasswordConfirmInput")
|
||||
.setValue("#changePasswordPagePasswordConfirmInput", "janicedoe123")
|
||||
.verify.cssProperty('#changePasswordPagePasswordConfirmInput', 'border', '1px solid green')
|
||||
},
|
||||
"if anonymous user tries to log in with non-existing account, a message is shown" : function (client) {
|
||||
client
|
||||
@ -165,18 +203,18 @@ module.exports = {
|
||||
.verify.containsText("#signInPageMessage", "User not found [403]")
|
||||
.verify.cssProperty("#signInPageMessage", "color", "rgba(169, 68, 66, 1)")
|
||||
.verify.cssProperty("#signInPageMessage", "background-color", "rgba(242, 222, 222, 1)")
|
||||
.verify.cssProperty("#signInPageMessage", "border-color", "rgb(235, 204, 209)")
|
||||
.verify.cssProperty("#signInPageMessage", "border-color", "rgb(235, 204, 209)");
|
||||
},
|
||||
"anonymous guest should be notified if email already exists" : function (client) {
|
||||
"anonymous guest should be notified if email already exists": function (client) {
|
||||
client
|
||||
.url("http://localhost:3000/entrySignUp")
|
||||
.resizeWindow(1024, 768)
|
||||
.signUp("janicedoe@symptomatic.io", "janicedoe123").pause(500)
|
||||
.click("#signUpPageEmailInput").pause(500)
|
||||
.signUp("janicedoe@symptomatic.io", "Janiced*e123").pause(500)
|
||||
.click("#signUpPageJoinNowButton").pause(1000)
|
||||
.verify.elementPresent("#signUpPageMessage")
|
||||
.verify.containsText("#signUpPageMessage", "Email already exists. [403]")
|
||||
.verify.containsText("#signUpPageMessage", "Email already exists. [403]");
|
||||
},
|
||||
after: function(client){
|
||||
after: function (client) {
|
||||
client
|
||||
.dropEntryUsers()
|
||||
.end();
|
||||
|
||||
@ -4,7 +4,23 @@
|
||||
<img src="/images/logo.png">
|
||||
<h4 class="name">Open Health Imaging Foundation</h4>
|
||||
</a>
|
||||
{{#if currentUser}}
|
||||
<div id="userProfile" class="row-fluid">
|
||||
<div class="dropdown clearfix">
|
||||
<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="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>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{> timeoutCountdownDialog}}
|
||||
{{ >optionsButton }}
|
||||
{{ >yield }}
|
||||
</template>
|
||||
@ -0,0 +1,44 @@
|
||||
Template.lesionTrackerLayout.events({
|
||||
'click #logoutButton': function() {
|
||||
Meteor.logout(function(){
|
||||
Router.go('/entrySignIn');
|
||||
});
|
||||
},
|
||||
'click #changePassword': function() {
|
||||
Router.go('/changePassword');
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTrackerLayout.helpers({
|
||||
'fullName': function() {
|
||||
return Meteor.user().profile.fullName;
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTrackerLayout.onCreated(function() {
|
||||
// Show countdown dialog
|
||||
var handle;
|
||||
$(document).on('TriggerOpenTimeoutCountdownDialog', function (e, leftTime) {
|
||||
// TODO: Show modal dialog
|
||||
handle = setInterval(function() {
|
||||
leftTime --;
|
||||
// Set countdownDialogLeftTime session
|
||||
Session.set("countdownDialogLeftTime", leftTime);
|
||||
// Show dialog
|
||||
if ($("#timeoutCountdownDialog").css("display") == "none") {
|
||||
$("#timeoutCountdownDialog").css("display", "block");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
});
|
||||
|
||||
$(document).on('TriggerCloseTimeoutCountdownDialog', function (e) {
|
||||
$("#timeoutCountdownDialog").css("display", "none");
|
||||
if (handle) {
|
||||
clearInterval(handle);
|
||||
// Close the dialog
|
||||
$("#timeoutCountdownDialog").css("display", "none");
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -7,12 +7,35 @@
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
width: 15%
|
||||
|
||||
.navbar-brand
|
||||
#userProfile
|
||||
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
|
||||
margin-right: 5px
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
img
|
||||
height: 30px
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<template name="timeoutCountdownDialog">
|
||||
<div id="timeoutCountdownDialog">
|
||||
<div class="dialogHeader">
|
||||
<h5>Session is about to expire!</h5>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<p>You will be log out in {{leftTime}} {{secondsText}}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,9 @@
|
||||
|
||||
Template.timeoutCountdownDialog.helpers({
|
||||
leftTime: function(e) {
|
||||
return Session.get("countdownDialogLeftTime");
|
||||
},
|
||||
secondsText: function() {
|
||||
return Session.get("countdownDialogLeftTime") > 1? "seconds": "second";
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
#timeoutCountdownDialog
|
||||
display: none
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
right: 0
|
||||
margin: auto
|
||||
width: 20%
|
||||
height: 15%
|
||||
z-index: 100
|
||||
border-radius: 5px
|
||||
padding: 10px 20px 10px 20px
|
||||
background-color: rgba(255,255,255,1)
|
||||
outline: none
|
||||
@ -52,6 +52,7 @@ Package.onUse(function(api) {
|
||||
// UI Components
|
||||
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.html', 'client');
|
||||
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.styl', 'client');
|
||||
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.js', 'client');
|
||||
|
||||
api.addFiles('client/components/associationModal/associationModal.html', 'client');
|
||||
api.addFiles('client/components/associationModal/associationModal.styl', 'client');
|
||||
@ -110,6 +111,9 @@ Package.onUse(function(api) {
|
||||
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html', 'client');
|
||||
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.styl', 'client');
|
||||
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js', 'client');
|
||||
api.addFiles('client/components/timeoutCountdownDialog/timeoutCountdownDialog.html', 'client');
|
||||
api.addFiles('client/components/timeoutCountdownDialog/timeoutCountdownDialog.styl', 'client');
|
||||
api.addFiles('client/components/timeoutCountdownDialog/timeoutCountdownDialog.js', 'client');
|
||||
|
||||
api.addFiles('client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.html', 'client');
|
||||
api.addFiles('client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.js', 'client');
|
||||
|
||||
20
Packages/meteor-stale-session/LICENSE
Normal file
20
Packages/meteor-stale-session/LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Chris Lindley
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
58
Packages/meteor-stale-session/README.md
Normal file
58
Packages/meteor-stale-session/README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# zuuk:stale-session
|
||||
|
||||
Stale session and session timeout handling for [meteorjs](http://www.meteor.com/).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```sh
|
||||
$ meteor add zuuk:stale-session
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
When a user logs in to a meteor application, they may gain access to privileged information and functionality. If they neglect to log off, another user of the same computer can effectively impersonate that user and gains the same rights. As it currently stands, (meteor 0.6.6.3), login tokens remain valid for eternity so this creates a large window of opportunity for impersonators.
|
||||
|
||||
This package is designed to detect a user's inactivity and automatically log them off after a configurable amount of time thereby reducing the size of this window to just the inactivity delay.
|
||||
|
||||
It is possible to configure both the timeout and the events that consitute activity.
|
||||
|
||||
The user will be logged off whether the browser window remains open or not.
|
||||
|
||||
The user is logged off by the server and disabling javascript in the browser (kind of pointless in meteor!) would not prevent automatic log off.
|
||||
|
||||
The user can be logged on multiple times on multiple devices and activity in any one of those devices will keep the sessions alive.
|
||||
|
||||
The plugin uses a heartbeat that is configurable but defaulted to ensure that the server is not inundated with heartbeats from clients in systems with many concurrent users.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is via `Meteor.settings.public`.
|
||||
|
||||
- `staleSessionInactivityTimeout` - the amount of time (in ms) after which, if no activity is noticed, a session will be considered stale - default 30 minutes.
|
||||
- `staleSessionPurgeInterval` - interval (in ms) at which stale sessions are purged i.e. found and forcibly logged out - default 1 minute.
|
||||
- `staleSessionHeartbeatInterval` - interval (in ms) at which activity heartbeats are sent up to the server - default every 3 minutes.
|
||||
- `staleSessionActivityEvents` - the jquery events which are considered indicator of activity e.g. in an on() call - default `mousemove click keydown`
|
||||
|
||||
You can set these variables in `config/settings.json` and then launch Meteor with `meteor --settings config/settings.json`.
|
||||
|
||||
Example `config/settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"public": {
|
||||
"staleSessionInactivityTimeout": 1800000,
|
||||
"staleSessionHeartbeatInterval": 180000,
|
||||
"staleSessionPurgeInterval": 60000,
|
||||
"staleSessionActivityEvents": "mousemove click keydown"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Background
|
||||
|
||||
A meteor project I was working on at [ZUUK](http://www.zuuk.com/), required user sessions to timeout after a period of inactivity. Meteor itself doesn't currently (0.6.6.3) support this out of the box and, though there were several plugins already available on [Atmosphere](https://atmosphere.meteor.com/), none of them worked reliably for me so I was forced to create my own for the project. I owe those other packages a great deal of gratitude as this package is effectively just taking ideas from them and making them work in a simpler more reliable fashion for my project. I'm putting this back into the community in the hope it will help in the same situation.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
86
Packages/meteor-stale-session/client.js
Normal file
86
Packages/meteor-stale-session/client.js
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// Client side activity detection for the session timeout
|
||||
// - depends on jquery
|
||||
//
|
||||
// Meteor settings:
|
||||
// - staleSessionHeartbeatInterval: interval (in ms) at which activity heartbeats are sent up to the server
|
||||
// - staleSessionActivityEvents: the jquery events which are considered indicator of activity e.g. in an on() call.
|
||||
//
|
||||
var heartbeatInterval = Meteor.settings && Meteor.settings.public && Meteor.settings.public.staleSessionHeartbeatInterval || (3*60*1000); // 3mins
|
||||
var activityEvents = Meteor.settings && Meteor.settings.public && Meteor.settings.public.staleSessionActivityEvents || 'mousemove click keydown';
|
||||
var inactivityTimeout = Meteor.settings && Meteor.settings.public && Meteor.settings.public.staleSessionInactivityTimeout || (30*60*1000); // 30mins
|
||||
|
||||
var dialogTimeout = Meteor.settings && Meteor.settings.public && Meteor.settings.public.dialogTimeout || (30*1000); // 30secs
|
||||
var showCountdownDialog = Meteor.settings && Meteor.settings.public && Meteor.settings.public.showCountdownDialog || false;
|
||||
var countdownHeartbeatInterval = (heartbeatInterval < dialogTimeout)? heartbeatInterval : dialogTimeout;
|
||||
|
||||
var activityDetected = false;
|
||||
var lastActivityDetectedTime = new Date();
|
||||
var dialogIsOpen = false;
|
||||
|
||||
Meteor.startup(function() {
|
||||
|
||||
//
|
||||
// periodically send a heartbeat if activity has been detected within the interval
|
||||
//
|
||||
if (showCountdownDialog) {
|
||||
Meteor.setInterval(function() {
|
||||
if (Meteor.userId()) {
|
||||
if (activityDetected) {
|
||||
Meteor.call('heartbeat', function(error, heartbeatTime) {
|
||||
lastActivityDetectedTime = heartbeatTime;
|
||||
activityDetected = false;
|
||||
// Event to close dialog
|
||||
$.event.trigger('TriggerCloseTimeoutCountdownDialog');
|
||||
dialogIsOpen = false;
|
||||
});
|
||||
|
||||
} else {
|
||||
var overdueTimestamp = new Date().getTime() - lastActivityDetectedTime;
|
||||
// Ignore min differences
|
||||
overdueTimestamp = overdueTimestamp - (overdueTimestamp % 1000);
|
||||
console.log(overdueTimestamp);
|
||||
var startTime = inactivityTimeout - dialogTimeout;
|
||||
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 {
|
||||
// Event to close dialog
|
||||
$.event.trigger('TriggerCloseTimeoutCountdownDialog');
|
||||
dialogIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Event to close dialog
|
||||
$.event.trigger('TriggerCloseTimeoutCountdownDialog');
|
||||
dialogIsOpen = false;
|
||||
}
|
||||
}, countdownHeartbeatInterval);
|
||||
|
||||
} else{
|
||||
|
||||
Meteor.setInterval(function() {
|
||||
if (Meteor.userId() && activityDetected) {
|
||||
Meteor.call('heartbeat');
|
||||
activityDetected = false;
|
||||
}
|
||||
}, heartbeatInterval);
|
||||
}
|
||||
|
||||
//
|
||||
// detect activity and mark it as detected on any of the following events
|
||||
//
|
||||
$(document).on(activityEvents, function() {
|
||||
activityDetected = true;
|
||||
// Event to close dialog
|
||||
$.event.trigger('TriggerCloseTimeoutCountdownDialog');
|
||||
dialogIsOpen = false;
|
||||
});
|
||||
});
|
||||
13
Packages/meteor-stale-session/package.js
Normal file
13
Packages/meteor-stale-session/package.js
Normal file
@ -0,0 +1,13 @@
|
||||
Package.describe({
|
||||
name: 'zuuk:stale-session',
|
||||
summary: 'Stale session and session timeout handling for meteorjs',
|
||||
git: "https://github.com/lindleycb/meteor-stale-session.git",
|
||||
version: "1.0.8"
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('accounts-base@1.0.0', ['client','server']);
|
||||
api.use('jquery@1.0.0', 'client');
|
||||
api.addFiles('client.js', 'client');
|
||||
api.addFiles('server.js', 'server');
|
||||
});
|
||||
37
Packages/meteor-stale-session/server.js
Normal file
37
Packages/meteor-stale-session/server.js
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// Server side activity detection for the session timeout
|
||||
//
|
||||
// Meteor settings:
|
||||
// - staleSessionInactivityTimeout: the amount of time (in ms) after which, if no activity is noticed, a session will be considered stale
|
||||
// - staleSessionPurgeInterval: interval (in ms) at which stale sessions are purged i.e. found and forcibly logged out
|
||||
//
|
||||
var staleSessionPurgeInterval = Meteor.settings && Meteor.settings.public && Meteor.settings.public.staleSessionPurgeInterval || (1*60*1000); // 1min
|
||||
var inactivityTimeout = Meteor.settings && Meteor.settings.public && Meteor.settings.public.staleSessionInactivityTimeout || (30*60*1000); // 30mins
|
||||
|
||||
//
|
||||
// provide a user activity heartbeat method which stamps the user record with a timestamp of the last
|
||||
// received activity heartbeat.
|
||||
//
|
||||
Meteor.methods({
|
||||
heartbeat: function(options) {
|
||||
if (!this.userId) { return; }
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (user) {
|
||||
var heartbeatTime = new Date();
|
||||
Meteor.users.update(user._id, {$set: {heartbeat: heartbeatTime}});
|
||||
return heartbeatTime;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//
|
||||
// periodically purge any stale sessions, removing their login tokens and clearing out the stale heartbeat.
|
||||
//
|
||||
Meteor.setInterval(function() {
|
||||
var now = new Date(), overdueTimestamp = new Date(now-inactivityTimeout);
|
||||
Meteor.users.update({heartbeat: {$lt: overdueTimestamp}},
|
||||
{$set: {'services.resume.loginTokens': []},
|
||||
$unset: {heartbeat:1}},
|
||||
{multi: true});
|
||||
}, staleSessionPurgeInterval);
|
||||
@ -15,5 +15,13 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"public": {
|
||||
"staleSessionInactivityTimeout": 300000,
|
||||
"staleSessionHeartbeatInterval": 30000,
|
||||
"staleSessionPurgeInterval": 15000,
|
||||
"staleSessionActivityEvents": "mousemove click keydown",
|
||||
"showCountdownDialog": true,
|
||||
"dialogTimeout": 30000
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user