Skip to content

Commit

Permalink
Merge pull request #63 from chikh/master
Browse files Browse the repository at this point in the history
#45 "forgot password" feature POC
  • Loading branch information
paveltiunov committed Dec 6, 2015
2 parents fdc9d97 + d4267a3 commit 5717ec3
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 1 deletion.
4 changes: 4 additions & 0 deletions allcount-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ function configure() {
'mongoBsonSerializationCompileService',
'referenceNameService'
]);
injection.bindMultiple('compileServices', [
'forgotPasswordService'
]);
injection.bindMultiple('appConfigs',['forgotPasswordModule']);
injection.bindFactory('mongoBsonSerializationCompileService', require('./services/mongo/mongo-bson-serialization-compile-service'));
injection.bindFactory('mongoFieldService', require('./services/mongo/mongo-field-service'));
injection.bindFactory('mongoDefaultFieldProvider', require('./services/mongo/mongo-default-field-provider'));
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"url": "https://github.com/allcount/allcountjs"
},
"dependencies": {
"allcountjs-mailgun": "^1.0.0",
"body-parser": "1.0.1",
"bower-installer": "^0.8.4",
"cloudinary": "^1.2.1",
Expand All @@ -56,7 +57,7 @@
"methods": "^1.1.1",
"minimist": "^1.1.0",
"mkdirp": "~0.3.5",
"moment": "2.6.0",
"moment": "^2.6.0",
"mongoose": "~3.8.21",
"mongoose-long": "^0.0.2",
"node-minify": "^1.2.1",
Expand Down
113 changes: 113 additions & 0 deletions services/forgot-password-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
var _ = require('lodash');
var moment = require('moment');

module.exports = function (appUtil, keygrip, forgotPasswordService, baseUrlService, A, injection) {
var service = {};

service.generateToken = function (entity) {
return keygrip.sign(entity.username) + keygrip.sign(entity.creationDate.toString());
};

A.app({
entities: function (Fields, Crud, Security) { //todo: remove direct dependency on MailgunService
var MailgunService = injection.inject('MailgunService', true);
return {
forgotPassword: {
fields: {
username: Fields.text('User').required(),
token: Fields.text('Token').readOnly(),
creationDate: Fields.date('Creation date').readOnly()
},
layout: {
V: ['username']
},
permissions: {
create: null
},
customView: 'forgotpassword/forgot-password',
views: {
resetPassword: {
fields: {
username: Fields.text('User').required(),
creationDate: Fields.date('Creation date').readOnly(),
newPasswordHash: Fields.password('New password').required(),
repeatNewPasswordHash: Fields.password('Repeat password').required(),
hasTokenBeenUsed: Fields.checkbox('Has token been used?')
},
layout: {
V: ['newPasswordHash', 'repeatNewPasswordHash']
},
customView: 'forgotpassword/reset-password',
beforeUpdate: function (NewEntity, OldEntity) {
if (OldEntity.hasTokenBeenUsed) {
throw new appUtil.ValidationError({
repeatNewPasswordHash: 'Token has been already used'
});
} else if (NewEntity.newPasswordHash != NewEntity.repeatNewPasswordHash) {
throw new appUtil.ValidationError({
repeatNewPasswordHash: 'Passwords doesn\'t match'
});
} else if (moment().subtract(15, 'minutes') > NewEntity.creationDate) {
throw new appUtil.ValidationError({
repeatNewPasswordHash: 'Token has expired'
});
} else {
var userCrud = Crud.crudForEntityType('User'); //todo: what if User entity type would be overridden?
return Security.asSystem(function () {
return userCrud.find({filtering: {username: NewEntity.username}});
}).then(function (users) {
if (!_.isEmpty(users) && users.length === 1) {
var user = users[0];
user.passwordHash = NewEntity.newPasswordHash;
return Security.asSystem(function () {
userCrud.updateEntity(user);
});
} else {
throw new Error('No users with name ' + NewEntity.username);
}
}).then(function () {
NewEntity.hasTokenBeenUsed = true;
});
}
}
}
},
beforeSave: function (Entity, Dates) {
if (Entity.newPasswordHash || Entity.repeatNewPasswordHash) {
return;
}
var userCrud = Crud.crudForEntityType('User'); //todo: what if User entity type would be overridden?
return Security.asSystem(function () {
return userCrud.find({filtering: {username: Entity.username}});
}).then(function (users) {
if (!_.isEmpty(users)) {
Entity.creationDate = Dates.now();
Entity.token = service.generateToken(Entity);
var user = users[0];
var transport = forgotPasswordService.config.propertyValue('transport').evaluateProperties();
return MailgunService.sendMessage({
domain: transport.config.domain,
key: transport.config.key,
message: {
from: transport.config.from,
to: user.email || user.mail || user.username,
subject: 'Password recovery',
text: 'Someone (maybe that was you) requested password change. Follow this ' +
'link to complete the action: ' +
[baseUrlService.getBaseUrl(), 'entity', 'resetPassword', Entity.token].join('/') +
' Or just ignore this email if it wasn\'t you.' //todo: clean the text and implement html message
}
});
} else {
throw new appUtil.ValidationError({
username: 'Can\'t find user with name "' + Entity.username + '"'
});
}
});

}
}
};
}
});
};
16 changes: 16 additions & 0 deletions services/forgot-password-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = function () {
var service = {};

service.config = {};

service.compile = function (objects) {
objects.forEach(function (obj) {
var config = obj.propertyValue('forgotPassword');
if (config) {
service.config = config;
}
});
};

return service;
};
36 changes: 36 additions & 0 deletions views/forgotpassword/forgot-password.jade
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
extends main
include mixins

block vars

block content
div(ng-app='allcount', ng-controller='EntityViewController' ng-cloak)
.container.screen-container(ng-controller='ForgotPasswordCtrl')
+defaultCreateForm()(ng-show="true")
+defaultFormTemplate()
.form-group
button.btn.btn-block.btn-default(lc-tooltip='Submit' ng-click='submitUsername()')
i(class='glyphicon glyphicon-ok')
.form-group
p.bg-success(ng-show='successMessage') {{successMessage}}
p.bg-danger(ng-show='errorMessage') {{errorMessage}}
block js
+entityJs()
script.
angular.module('allcount').controller('ForgotPasswordCtrl', ['$scope', 'lcApi', '$q', function ($scope, lcApi, $q) {
$scope.submitUsername = function () {
return $q.when($scope.viewState.createForm.entity().username).then(function (username) {
return lcApi.createEntity({entityTypeId: 'forgotPassword'}, {username: username});
}).then(function (id) {
if (id) {
$scope.successMessage = 'Further instructions are sent';
delete $scope.errorMessage;
}
}, function (err) {
if (err) {
delete $scope.successMessage;
$scope.errorMessage = err.data.username;
}
});
}
}]);
53 changes: 53 additions & 0 deletions views/forgotpassword/reset-password.jade
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
extends main
include mixins

block vars

block content
div(ng-app='allcount', ng-controller='EntityViewController' ng-cloak)
.container.screen-container(ng-controller='ResetPasswordCtrl')
+defaultEditForm()(ng-show='doesTokenExists' entity-id='actualEntityId' is-editor='true')
+defaultFormTemplate()
.form-group
button.btn.btn-block.btn-default(lc-tooltip='Submit' ng-click='submitPassword()' ng-show='doesTokenExists')
i(class='glyphicon glyphicon-ok')
.form-group
p.bg-danger(ng-show='errorMessage') {{errorMessage}}
p.bg-success(ng-show='successMessage') {{successMessage}}
block js
+entityJs()
script.
angular.module('allcount').controller('ResetPasswordCtrl', ['$scope', 'lcApi', '$q', function ($scope, lcApi, $q) {
$scope.doesTokenExists = false;
$scope.successMessage = undefined;
$scope.errorMessage = undefined;
$scope.$watch('viewState.formEntityId', function (token) {
return lcApi.findRange({entityTypeId: 'forgotPassword'}, {
filtering: {
token: token
}
}).then(function (tokens) {
if (tokens && tokens.length > 0) {
$scope.doesTokenExists = true;
$scope.actualEntityId = tokens[0].id;
} else {
$scope.errorMessage = 'Token doesn\'t exists or it has been expired'
$scope.successMessage = undefined;
}
});
});
$scope.submitPassword = function () {
return $q.when($scope.viewState.editForm.entity()).then(function (entity) {
return lcApi.updateEntity({entityTypeId: 'resetPassword'}, entity);
}).then(function (res) {
if (res.username) {
$scope.successMessage = 'Password has been successfully changed';
$scope.errorMessage = undefined;
}
}, function (err) {
$scope.errorMessage = err.data.repeatNewPasswordHash || err;
$scope.successMessage = undefined;
});
};
}]);

3 changes: 3 additions & 0 deletions views/login.jade
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ block content
.login-or= messages('Or')
each method in loginMethods
a.btn.btn-lg.btn-primary.btn-block(href=method.url)= messages(method.label)
h4
a(href='/entity/forgotPassword')
p Forgot password?

block js
script(src="/assets/js/login.js")

0 comments on commit 5717ec3

Please sign in to comment.