diff --git a/app/controllers/users.server.controller.js b/app/controllers/users.server.controller.js index b8b4c76f6f..4a4884f081 100755 --- a/app/controllers/users.server.controller.js +++ b/app/controllers/users.server.controller.js @@ -7,6 +7,12 @@ var mongoose = require('mongoose'), passport = require('passport'), User = mongoose.model('User'), _ = require('lodash'); + /* Requires for reset password */ +var nodemailer = require('nodemailer'); +var LocalStrategy = require('passport-local').Strategy; +var bcrypt = require('bcrypt-nodejs'); +var async = require('async'); +var crypto = require('crypto'); /** * Get the error message from error object @@ -92,6 +98,160 @@ exports.signin = function(req, res, next) { })(req, res, next); }; +/** + * Forgot for reset password (forgot POST) + */ +exports.forgot = function(req, res, next) { + async.waterfall([ + // Generate random token + function(done) { + crypto.randomBytes(20, function(err, buf) { + var token = buf.toString('hex'); + done(err, token); + }); + }, + // Lookup user by email address + function(token, done) { + if (req.body.email) { + User.findOne({ email: req.body.email }, function(err, user) { + if (!user) { + return res.send(400, { + message: 'No account with that email address exists' + }); + } + + user.resetPasswordToken = token; + user.resetPasswordExpires = Date.now() + 3600000; // 1 hour + + user.save(function(err) { + done(err, token, user); + }); + }); + } else { + return res.send(400, { + message: 'Email field must not be blank' + }); + } + + }, + // If valid email, send reset email using service + function(token, user, done) { + var smtpTransport = nodemailer.createTransport('SMTP', { + service: 'SendGrid', // Choose email service, default SendGrid + auth: { + user: 'your_sendgrid_email@domain.com', + pass: 'your_sendgrid_password' + } + }); + var mailOptions = { + to: user.email, + from: 'your_email@domain.com', + subject: 'Password Reset', + text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' + + 'Please click on the following link, or paste this into your browser to complete the process:\n\n' + + 'http://' + req.headers.host + '/auth/reset/' + token + '\n\n' + + 'If you did not request this, please ignore this email and your password will remain unchanged.\n' + }; + smtpTransport.sendMail(mailOptions, function(err) { + res.send(200, { + message: 'An email has been sent to ' + user.email + ' with further instructions.' + }); + done(err, 'done'); + }); + } + ], function(err) { + if (err) return next(err); + }); +}; + +/** + * Reset password GET from email token + */ +exports.resetGet = function(req, res) { + User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) { + if (!user) { + // res.render('404'); + res.send(400, { + message: 'Password reset token is invalid or has expired.' + }); + return res.redirect('/#!/forgot'); + } + + res.redirect('/#!/reset/' + req.params.token); + + }); +}; + +/** + * Reset password POST from email token + */ +exports.resetPost = function(req, res) { + // Init Variables + var passwordDetails = req.body; + var message = null; + + async.waterfall([ + function(done) { + User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function(err, user) { + if (!err && user) { + if (passwordDetails.newPassword === passwordDetails.verifyPassword) { + user.password = passwordDetails.newPassword; + user.resetPasswordToken = undefined; + user.resetPasswordExpires = undefined; + + user.save(function(err) { + if (err) { + return res.send(400, { + message: getErrorMessage(err) + }); + } else { + req.login(user, function(err) { + if (err) { + res.send(400, err); + } else { + done(err, user); + } + }); + } + }); + } else { + return res.send(400, { + message: 'Passwords do not match' + }); + } + } else { + return res.send(400, { + message: 'Password reset token is invalid or has expired.' + }); + } + }); + }, + function(user, done) { + var smtpTransport = nodemailer.createTransport('SMTP', { + service: 'SendGrid', + auth: { + user: 'your_sendgrid_email@domain.com', + pass: 'your_sendgrid_password' + } + }); + var mailOptions = { + to: user.email, + from: 'your_email@domain.com', + subject: 'Your password has been changed', + text: 'Hello,\n\n' + + 'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n' + }; + smtpTransport.sendMail(mailOptions, function(err) { + res.send(200, { + message: 'Password changed successfully' + }); + }); + } + ], function(err) { + res.redirect('/'); + }); +}; + /** * Update user details */ diff --git a/app/models/user.server.model.js b/app/models/user.server.model.js index 6cfe08c231..6af0c8b3ad 100755 --- a/app/models/user.server.model.js +++ b/app/models/user.server.model.js @@ -81,7 +81,14 @@ var UserSchema = new Schema({ created: { type: Date, default: Date.now - } + }, + /* For reset password */ + resetPasswordToken: { + type: String + }, + resetPasswordExpires: { + type: Date + } }); /** diff --git a/app/routes/users.server.routes.js b/app/routes/users.server.routes.js index f925eb0618..d7cd2fa71b 100644 --- a/app/routes/users.server.routes.js +++ b/app/routes/users.server.routes.js @@ -17,6 +17,9 @@ module.exports = function(app) { app.route('/auth/signup').post(users.signup); app.route('/auth/signin').post(users.signin); app.route('/auth/signout').get(users.signout); + app.route('/auth/forgot').post(users.forgot); + app.route('/auth/reset/:token').get(users.resetGet); + app.route('/auth/reset/:token').post(users.resetPost); // Setting the facebook oauth routes app.route('/auth/facebook').get(passport.authenticate('facebook', { diff --git a/package.json b/package.json index bf7aed985c..7ff7db95d4 100755 --- a/package.json +++ b/package.json @@ -41,7 +41,10 @@ "forever": "~0.11.0", "bower": "~1.3.1", "grunt-cli": "~0.1.13", - "glob": "~3.2.9" + "glob": "~3.2.9", + "bcrypt-nodejs": "0.0.3", + "async": "~0.8.0", + "nodemailer": "~0.6.3" }, "devDependencies": { "supertest": "~0.10.0", diff --git a/public/dist/application.min.js b/public/dist/application.min.js index 62a5f946a5..059a714d7f 100644 --- a/public/dist/application.min.js +++ b/public/dist/application.min.js @@ -1 +1 @@ -"use strict";var ApplicationConfiguration=function(){return{applicationModuleName:"mean",applicationModuleVendorDependencies:["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils"],registerModule:function(moduleName){angular.module(moduleName,[]),angular.module(this.applicationModuleName).requires.push(moduleName)}}}();angular.module(ApplicationConfiguration.applicationModuleName,ApplicationConfiguration.applicationModuleVendorDependencies),angular.module(ApplicationConfiguration.applicationModuleName).config(["$locationProvider",function($locationProvider){$locationProvider.hashPrefix("!")}]),angular.element(document).ready(function(){"#_=_"===window.location.hash&&(window.location.hash="#!"),angular.bootstrap(document,[ApplicationConfiguration.applicationModuleName])}),ApplicationConfiguration.registerModule("articles"),ApplicationConfiguration.registerModule("core"),ApplicationConfiguration.registerModule("users"),angular.module("articles").run(["Menus",function(Menus){Menus.addMenuItem("topbar","Articles","articles"),Menus.addMenuItem("topbar","New Article","articles/create")}]),angular.module("articles").config(["$stateProvider",function($stateProvider){$stateProvider.state("listArticles",{url:"/articles",templateUrl:"modules/articles/views/list-articles.client.view.html"}).state("createArticle",{url:"/articles/create",templateUrl:"modules/articles/views/create-article.client.view.html"}).state("viewArticle",{url:"/articles/:articleId",templateUrl:"modules/articles/views/view-article.client.view.html"}).state("editArticle",{url:"/articles/:articleId/edit",templateUrl:"modules/articles/views/edit-article.client.view.html"})}]),angular.module("articles").controller("ArticlesController",["$scope","$stateParams","$location","Authentication","Articles",function($scope,$stateParams,$location,Authentication,Articles){$scope.authentication=Authentication,$scope.create=function(){var article=new Articles({title:this.title,content:this.content});article.$save(function(response){$location.path("articles/"+response._id)},function(errorResponse){$scope.error=errorResponse.data.message}),this.title="",this.content=""},$scope.remove=function(article){if(article){article.$remove();for(var i in $scope.articles)$scope.articles[i]===article&&$scope.articles.splice(i,1)}else $scope.article.$remove(function(){$location.path("articles")})},$scope.update=function(){var article=$scope.article;article.updated||(article.updated=[]),article.updated.push((new Date).getTime()),article.$update(function(){$location.path("articles/"+article._id)},function(errorResponse){$scope.error=errorResponse.data.message})},$scope.find=function(){Articles.query(function(articles){$scope.articles=articles})},$scope.findOne=function(){Articles.get({articleId:$stateParams.articleId},function(article){$scope.article=article})}}]),angular.module("articles").factory("Articles",["$resource",function($resource){return $resource("articles/:articleId",{articleId:"@_id"},{update:{method:"PUT"}})}]),angular.module("core").config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider){$urlRouterProvider.otherwise("/"),$stateProvider.state("home",{url:"/",templateUrl:"modules/core/views/home.client.view.html"})}]),angular.module("core").controller("HeaderController",["$scope","Authentication","Menus",function($scope,Authentication,Menus){$scope.authentication=Authentication,$scope.isCollapsed=!1,$scope.menu=Menus.getMenu("topbar"),$scope.toggleCollapsibleMenu=function(){$scope.isCollapsed=!$scope.isCollapsed}}]),angular.module("core").controller("HomeController",["$scope","Authentication",function($scope,Authentication){$scope.authentication=Authentication}]),angular.module("core").service("Menus",[function(){this.defaultRoles=["user"],this.menus={};var shouldRender=function(user){if(!user)return!this.requiresAuthentication;for(var userRoleIndex in user.roles)for(var roleIndex in this.roles)if(this.roles[roleIndex]===user.roles[userRoleIndex])return!0;return!1};this.validateMenuExistance=function(menuId){if(menuId&&menuId.length){if(this.menus[menuId])return!0;throw new Error("Menu does not exists")}throw new Error("MenuId was not provided")},this.getMenu=function(menuId){return this.validateMenuExistance(menuId),this.menus[menuId]},this.addMenu=function(menuId,requiresAuthentication,roles){return this.menus[menuId]={requiresAuthentication:requiresAuthentication||!0,roles:roles||this.defaultRoles,items:[],shouldRender:shouldRender},this.menus[menuId]},this.removeMenu=function(menuId){this.validateMenuExistance(menuId),delete this.menus[menuId]},this.addMenuItem=function(menuId,menuItemTitle,menuItemURL,menuItemUIRoute,requiresAuthentication,roles){return this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:menuItemTitle,link:menuItemURL,uiRoute:menuItemUIRoute||"/"+menuItemURL,requiresAuthentication:requiresAuthentication||!1,roles:roles||this.defaultRoles,shouldRender:shouldRender}),this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].menuItemURL===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.addMenu("topbar")}]),angular.module("users").config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$q","$location","Authentication",function($q,$location,Authentication){return{responseError:function(rejection){switch(rejection.status){case 401:Authentication.user=null,$location.path("signin");break;case 403:}return $q.reject(rejection)}}}])}]),angular.module("users").config(["$stateProvider",function($stateProvider){$stateProvider.state("profile",{url:"/settings/profile",templateUrl:"modules/users/views/settings/edit-profile.client.view.html"}).state("password",{url:"/settings/password",templateUrl:"modules/users/views/settings/change-password.client.view.html"}).state("accounts",{url:"/settings/accounts",templateUrl:"modules/users/views/settings/social-accounts.client.view.html"}).state("signup",{url:"/signup",templateUrl:"modules/users/views/signup.client.view.html"}).state("signin",{url:"/signin",templateUrl:"modules/users/views/signin.client.view.html"})}]),angular.module("users").controller("AuthenticationController",["$scope","$http","$location","Authentication",function($scope,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.signup=function(){$http.post("/auth/signup",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})},$scope.signin=function(){$http.post("/auth/signin",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})}}]),angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/"),$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$scope.isConnectedSocialAccount=function(provider){return $scope.user.provider===provider||$scope.user.additionalProvidersData&&$scope.user.additionalProvidersData[provider]},$scope.removeUserSocialAccount=function(provider){$scope.success=$scope.error=null,$http.delete("/users/accounts",{params:{provider:provider}}).success(function(response){$scope.success=!0,$scope.user=Authentication.user=response}).error(function(response){$scope.error=response.message})},$scope.updateUserProfile=function(){$scope.success=$scope.error=null;var user=new Users($scope.user);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})},$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]),angular.module("users").factory("Authentication",[function(){var _this=this;return _this._data={user:window.user},_this._data}]),angular.module("users").factory("Users",["$resource",function($resource){return $resource("users",{},{update:{method:"PUT"}})}]); \ No newline at end of file +"use strict";var ApplicationConfiguration=function(){var applicationModuleName="mean",applicationModuleVendorDependencies=["ngResource","ngAnimate","ui.router","ui.bootstrap","ui.utils"],registerModule=function(moduleName){angular.module(moduleName,[]),angular.module(applicationModuleName).requires.push(moduleName)};return{applicationModuleName:applicationModuleName,applicationModuleVendorDependencies:applicationModuleVendorDependencies,registerModule:registerModule}}();angular.module(ApplicationConfiguration.applicationModuleName,ApplicationConfiguration.applicationModuleVendorDependencies),angular.module(ApplicationConfiguration.applicationModuleName).config(["$locationProvider",function($locationProvider){$locationProvider.hashPrefix("!")}]),angular.element(document).ready(function(){"#_=_"===window.location.hash&&(window.location.hash="#!"),angular.bootstrap(document,[ApplicationConfiguration.applicationModuleName])}),ApplicationConfiguration.registerModule("articles"),ApplicationConfiguration.registerModule("core"),ApplicationConfiguration.registerModule("users"),angular.module("articles").run(["Menus",function(Menus){Menus.addMenuItem("topbar","Articles","articles"),Menus.addMenuItem("topbar","New Article","articles/create")}]),angular.module("articles").config(["$stateProvider",function($stateProvider){$stateProvider.state("listArticles",{url:"/articles",templateUrl:"modules/articles/views/list-articles.client.view.html"}).state("createArticle",{url:"/articles/create",templateUrl:"modules/articles/views/create-article.client.view.html"}).state("viewArticle",{url:"/articles/:articleId",templateUrl:"modules/articles/views/view-article.client.view.html"}).state("editArticle",{url:"/articles/:articleId/edit",templateUrl:"modules/articles/views/edit-article.client.view.html"})}]),angular.module("articles").controller("ArticlesController",["$scope","$stateParams","$location","Authentication","Articles",function($scope,$stateParams,$location,Authentication,Articles){$scope.authentication=Authentication,$scope.create=function(){var article=new Articles({title:this.title,content:this.content});article.$save(function(response){$location.path("articles/"+response._id)},function(errorResponse){$scope.error=errorResponse.data.message}),this.title="",this.content=""},$scope.remove=function(article){if(article){article.$remove();for(var i in $scope.articles)$scope.articles[i]===article&&$scope.articles.splice(i,1)}else $scope.article.$remove(function(){$location.path("articles")})},$scope.update=function(){var article=$scope.article;article.$update(function(){$location.path("articles/"+article._id)},function(errorResponse){$scope.error=errorResponse.data.message})},$scope.find=function(){$scope.articles=Articles.query()},$scope.findOne=function(){$scope.article=Articles.get({articleId:$stateParams.articleId})}}]),angular.module("articles").factory("Articles",["$resource",function($resource){return $resource("articles/:articleId",{articleId:"@_id"},{update:{method:"PUT"}})}]),angular.module("core").config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider){$urlRouterProvider.otherwise("/"),$stateProvider.state("home",{url:"/",templateUrl:"modules/core/views/home.client.view.html"})}]),angular.module("core").controller("HeaderController",["$scope","Authentication","Menus",function($scope,Authentication,Menus){$scope.authentication=Authentication,$scope.isCollapsed=!1,$scope.menu=Menus.getMenu("topbar"),$scope.toggleCollapsibleMenu=function(){$scope.isCollapsed=!$scope.isCollapsed}}]),angular.module("core").controller("HomeController",["$scope","Authentication",function($scope,Authentication){$scope.authentication=Authentication}]),angular.module("core").service("Menus",[function(){this.defaultRoles=["user"],this.menus={};var shouldRender=function(user){if(!user)return this.isPublic;for(var userRoleIndex in user.roles)for(var roleIndex in this.roles)if(this.roles[roleIndex]===user.roles[userRoleIndex])return!0;return!1};this.validateMenuExistance=function(menuId){if(menuId&&menuId.length){if(this.menus[menuId])return!0;throw new Error("Menu does not exists")}throw new Error("MenuId was not provided")},this.getMenu=function(menuId){return this.validateMenuExistance(menuId),this.menus[menuId]},this.addMenu=function(menuId,isPublic,roles){return this.menus[menuId]={isPublic:isPublic||!1,roles:roles||this.defaultRoles,items:[],shouldRender:shouldRender},this.menus[menuId]},this.removeMenu=function(menuId){this.validateMenuExistance(menuId),delete this.menus[menuId]},this.addMenuItem=function(menuId,menuItemTitle,menuItemURL,menuItemUIRoute,isPublic,roles){return this.validateMenuExistance(menuId),this.menus[menuId].items.push({title:menuItemTitle,link:menuItemURL,uiRoute:menuItemUIRoute||"/"+menuItemURL,isPublic:isPublic||this.menus[menuId].isPublic,roles:roles||this.defaultRoles,shouldRender:shouldRender}),this.menus[menuId]},this.removeMenuItem=function(menuId,menuItemURL){this.validateMenuExistance(menuId);for(var itemIndex in this.menus[menuId].items)this.menus[menuId].items[itemIndex].link===menuItemURL&&this.menus[menuId].items.splice(itemIndex,1);return this.menus[menuId]},this.addMenu("topbar")}]),angular.module("users").config(["$httpProvider",function($httpProvider){$httpProvider.interceptors.push(["$q","$location","Authentication",function($q,$location,Authentication){return{responseError:function(rejection){switch(rejection.status){case 401:Authentication.user=null,$location.path("signin");break;case 403:}return $q.reject(rejection)}}}])}]),angular.module("users").config(["$stateProvider",function($stateProvider){$stateProvider.state("profile",{url:"/settings/profile",templateUrl:"modules/users/views/settings/edit-profile.client.view.html"}).state("password",{url:"/settings/password",templateUrl:"modules/users/views/settings/change-password.client.view.html"}).state("accounts",{url:"/settings/accounts",templateUrl:"modules/users/views/settings/social-accounts.client.view.html"}).state("signup",{url:"/signup",templateUrl:"modules/users/views/signup.client.view.html"}).state("signin",{url:"/signin",templateUrl:"modules/users/views/signin.client.view.html"})}]),angular.module("users").controller("AuthenticationController",["$scope","$http","$location","Authentication",function($scope,$http,$location,Authentication){$scope.authentication=Authentication,$scope.authentication.user&&$location.path("/"),$scope.signup=function(){$http.post("/auth/signup",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})},$scope.signin=function(){$http.post("/auth/signin",$scope.credentials).success(function(response){$scope.authentication.user=response,$location.path("/")}).error(function(response){$scope.error=response.message})}}]),angular.module("users").controller("SettingsController",["$scope","$http","$location","Users","Authentication",function($scope,$http,$location,Users,Authentication){$scope.user=Authentication.user,$scope.user||$location.path("/"),$scope.hasConnectedAdditionalSocialAccounts=function(){for(var i in $scope.user.additionalProvidersData)return!0;return!1},$scope.isConnectedSocialAccount=function(provider){return $scope.user.provider===provider||$scope.user.additionalProvidersData&&$scope.user.additionalProvidersData[provider]},$scope.removeUserSocialAccount=function(provider){$scope.success=$scope.error=null,$http.delete("/users/accounts",{params:{provider:provider}}).success(function(response){$scope.success=!0,$scope.user=Authentication.user=response}).error(function(response){$scope.error=response.message})},$scope.updateUserProfile=function(){$scope.success=$scope.error=null;var user=new Users($scope.user);user.$update(function(response){$scope.success=!0,Authentication.user=response},function(response){$scope.error=response.data.message})},$scope.changeUserPassword=function(){$scope.success=$scope.error=null,$http.post("/users/password",$scope.passwordDetails).success(function(){$scope.success=!0,$scope.passwordDetails=null}).error(function(response){$scope.error=response.message})}}]),angular.module("users").factory("Authentication",[function(){var _this=this;return _this._data={user:window.user},_this._data}]),angular.module("users").factory("Users",["$resource",function($resource){return $resource("users",{},{update:{method:"PUT"}})}]); \ No newline at end of file diff --git a/public/modules/users/config/users.client.routes.js b/public/modules/users/config/users.client.routes.js index e715870ad4..8b35fac649 100755 --- a/public/modules/users/config/users.client.routes.js +++ b/public/modules/users/config/users.client.routes.js @@ -24,6 +24,14 @@ angular.module('users').config(['$stateProvider', state('signin', { url: '/signin', templateUrl: 'modules/users/views/signin.client.view.html' + }). + state('forgot', { + url: '/forgot', + templateUrl: 'modules/users/views/forgot.client.view.html' + }). + state('reset', { + url: '/reset/:token', + templateUrl: 'modules/users/views/reset.client.view.html' }); } ]); \ No newline at end of file diff --git a/public/modules/users/controllers/authentication.client.controller.js b/public/modules/users/controllers/authentication.client.controller.js index a4ac880a25..46b88b561f 100644 --- a/public/modules/users/controllers/authentication.client.controller.js +++ b/public/modules/users/controllers/authentication.client.controller.js @@ -1,7 +1,7 @@ 'use strict'; -angular.module('users').controller('AuthenticationController', ['$scope', '$http', '$location', 'Authentication', - function($scope, $http, $location, Authentication) { +angular.module('users').controller('AuthenticationController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', + function($scope, $stateParams, $http, $location, Authentication) { $scope.authentication = Authentication; //If user is signed in then redirect back home @@ -30,5 +30,37 @@ angular.module('users').controller('AuthenticationController', ['$scope', '$http $scope.error = response.message; }); }; + + $scope.forgot = function() { + $scope.success = $scope.error = null; + + $http.post('/auth/forgot', $scope.credentials).success(function(response) { + // Show user success message and clear form + $scope.credentials = null; + $scope.success = response.message; + + }).error(function(response) { + // Show user error message and clear form + $scope.credentials = null; + $scope.error = response.message; + }); + }; + + // Change user password + $scope.reset = function() { + $scope.success = $scope.error = null; + + $http.post('/auth/reset/' + $stateParams.token, + $scope.passwordDetails).success(function(response) { + + // If successful show success message and clear form + $scope.success = response.message; + $scope.passwordDetails = null; + + }).error(function(response) { + $scope.error = response.message; + }); + }; + } ]); \ No newline at end of file diff --git a/public/modules/users/views/forgot.client.view.html b/public/modules/users/views/forgot.client.view.html new file mode 100644 index 0000000000..19213dce7f --- /dev/null +++ b/public/modules/users/views/forgot.client.view.html @@ -0,0 +1,21 @@ +
+

Forgot your password?

+
+ +
+
\ No newline at end of file diff --git a/public/modules/users/views/reset.client.view.html b/public/modules/users/views/reset.client.view.html new file mode 100644 index 0000000000..5d1b2f4efb --- /dev/null +++ b/public/modules/users/views/reset.client.view.html @@ -0,0 +1,26 @@ +
+

Reset your password

+
+ +
+
\ No newline at end of file diff --git a/public/modules/users/views/signin.client.view.html b/public/modules/users/views/signin.client.view.html index bdf035a6d4..1511a0625d 100644 --- a/public/modules/users/views/signin.client.view.html +++ b/public/modules/users/views/signin.client.view.html @@ -30,6 +30,9 @@

Or with your account

  or  Sign up +
+ Forgot your password? +
{{error}}