feat(users): add feature for verify mail address and active account by mail

This commit is contained in:
OldHawk
2017-11-06 15:01:43 +08:00
parent c34ec9736d
commit 6076ec0a1f
14 changed files with 278 additions and 121 deletions

View File

@@ -156,6 +156,7 @@ module.exports = {
*/
sign: {
openSignup: true,
signUpActiveTokenExpires: 60 * 60 * 1000 * 2,
allowSocialSignin: false,
showDemoSignMessage: true
},

View File

@@ -144,6 +144,7 @@
//page title
PAGETITLE: {
ACCOUNT_ACTIVE: 'Account Active',
UPLOAD: 'Upload',
MOVIE_LIST: 'Movie List',
TV_LIST: 'TV List',
@@ -229,7 +230,12 @@
ENTER_USERNAME: 'Enter your account username or email.',
RESET_PASS_OK: 'Password successfully reset',
RESET_PASS_INVALID: 'Password reset is invalid',
RE_RESET_PASSWORD: 'Ask for a new password reset?'
RE_RESET_PASSWORD: 'Ask for a new password reset?',
ACTIVE_INVALID: 'Can not active your account, maybe this token already used or this token is expired.',
ACTIVE_ERROR: 'Account active ERROR!',
ACTIVE_FAILED: 'Account active and login failed',
ACTIVE_SUCCESSFULLY: 'Account active successfully, will redirect to home after 3 seconds automate.'
},
//TorrentsController & views
@@ -1143,6 +1149,11 @@
ANDROID: 'Android',
CAR: 'Car'
}
},
//server returned string
SERVER: {
SENDING_ACTIVE_MAIL_SUCCESSFULLY: 'Welcome join <strong>{{site}}</strong>, We`ve sent you an email to <strong>{{mail}}</strong>, please check you mail box and click the active url to verify you mail address and active you account in <strong>{{hours}}</strong> hours, thanks!'
}
};

View File

@@ -144,6 +144,7 @@
//page title
PAGETITLE: {
ACCOUNT_ACTIVE: '帐号激活',
UPLOAD: '上传',
MOVIE_LIST: '电影列表',
TV_LIST: '电视剧列表',
@@ -229,7 +230,12 @@
ENTER_USERNAME: '请输入你的帐号用户名或注册邮箱',
RESET_PASS_OK: '密码重置成功',
RESET_PASS_INVALID: '密码重置失败',
RE_RESET_PASSWORD: '再次请求重置密码?'
RE_RESET_PASSWORD: '再次请求重置密码?',
ACTIVE_INVALID: '帐户激活失败, 可能该链接已被使用或者已失效.',
ACTIVE_ERROR: '帐户激活错误!',
ACTIVE_FAILED: '帐户激活与登录失败',
ACTIVE_SUCCESSFULLY: '帐户激活成功, 3秒钟后将自动跳转到首页.'
},
//TorrentsController & views
@@ -1143,6 +1149,11 @@
ANDROID: 'Android',
CAR: 'Car'
}
},
//server returned string
SERVER: {
SENDING_ACTIVE_MAIL_SUCCESSFULLY: '欢迎加入 <strong>{{site}}</strong>, 我们已向你的邮箱 <strong>{{mail}}</strong> 发送了一封电子邮件, 请在 <strong>{{hours}}</strong> 小时内检查您的邮箱并点击邮件中的链接地址来验证您的邮箱地址并激您的帐号,谢谢!'
}
};

View File

@@ -30,6 +30,10 @@
margin-top: 50px;
}
.margin-top-100 {
margin-top: 100px;
}
.margin-bottom-5 {
margin-bottom: 5px;
}
@@ -114,6 +118,10 @@
padding-top: 50px;
}
.padding-top-100 {
padding-top: 100px;
}
.padding-bottom-10 {
padding-bottom: 10px;
}

View File

@@ -40,6 +40,10 @@ body {
}
}
.active-notice {
font-size: 18px;
}
.backdrop {
background-image: url("http://image.tmdb.org/t/p/w1280/5pAGnkFYSsFJ99ZxDIYnhQbQFXs.jpg");
background-position: center;

View File

@@ -149,6 +149,15 @@
pageTitle: 'SIGNIN'
}
})
.state('authentication.active', {
url: '/active?method',
templateUrl: '/modules/users/client/views/authentication/active.client.view.html',
controller: 'AuthenticationController',
controllerAs: 'vm',
data: {
pageTitle: 'PAGETITLE.ACCOUNT_ACTIVE'
}
})
.state('authentication.invite', {
abstract: true,
url: '/invite',

View File

@@ -5,15 +5,16 @@
.module('users')
.controller('AuthenticationController', AuthenticationController);
AuthenticationController.$inject = ['$scope', '$state', 'UsersService', '$location', '$window', 'Authentication', 'PasswordValidator', 'Notification',
AuthenticationController.$inject = ['$scope', '$state', 'UsersService', '$location', '$window', '$timeout', 'Authentication', 'PasswordValidator', 'Notification',
'MeanTorrentConfig', 'getStorageLangService', '$rootScope', '$stateParams', 'InvitationsService'];
function AuthenticationController($scope, $state, UsersService, $location, $window, Authentication, PasswordValidator, Notification, MeanTorrentConfig,
function AuthenticationController($scope, $state, UsersService, $location, $window, $timeout, Authentication, PasswordValidator, Notification, MeanTorrentConfig,
getStorageLangService, $rootScope, $stateParams, InvitationsService) {
var vm = this;
vm.lang = getStorageLangService.getLang();
vm.signConfig = MeanTorrentConfig.meanTorrentConfig.sign;
vm.appConfig = MeanTorrentConfig.meanTorrentConfig.app;
vm.authentication = Authentication;
vm.getPopoverMsg = PasswordValidator.getPopoverMsg;
vm.signup = signup;
@@ -22,14 +23,19 @@
vm.usernameRegex = /^(?=[\w.-]+$)(?!.*[._-]{2})(?!\.)(?!.*\.$).{3,34}$/;
vm.credentials = {};
vm.activeMethod = $state.params.method;
// Get an eventual error defined in the URL query string:
if ($location.search().err) {
Notification.error({message: $location.search().err});
}
// If user is signed in then redirect back home
if (vm.authentication.user) {
$location.path('/');
/**
* account active successfully, redirect to home after 2 seconds
*/
if (vm.activeMethod === 'successfully') {
$timeout(function () {
$state.go('home');
}, 3000);
}
/**
@@ -69,6 +75,16 @@
UsersService.userSignup(vm.credentials)
.then(onUserSignupSuccess)
.catch(onUserSignupError);
function onUserSignupSuccess(response) {
vm.waitToActive = true;
vm.waitToActiveTranslate = response.message;
}
function onUserSignupError(response) {
Notification.error({message: response.data.message, title: '<i class="glyphicon glyphicon-remove"></i> Signup Error!', delay: 6000});
}
}
/**
@@ -87,6 +103,20 @@
UsersService.userSignin(vm.credentials)
.then(onUserSigninSuccess)
.catch(onUserSigninError);
function onUserSigninSuccess(response) {
// If successful we assign the response to the global user model
vm.authentication.user = response;
$rootScope.$broadcast('auth-user-changed');
$rootScope.$broadcast('user-invitations-changed');
Notification.info({message: 'Welcome ' + response.firstName});
// And redirect to the previous or home page
$state.go($state.previous.state.name || 'home', $state.previous.params);
}
function onUserSigninError(response) {
Notification.error({message: response.data.message, title: '<i class="glyphicon glyphicon-remove"></i> Signin Error!', delay: 6000});
}
}
// OAuth provider request
@@ -103,51 +133,6 @@
$window.location.href = url;
}
// Authentication Callbacks
/**
* onUserSignupSuccess
* @param response
*/
function onUserSignupSuccess(response) {
// If successful we assign the response to the global user model
vm.authentication.user = response;
$rootScope.$broadcast('auth-user-changed');
$rootScope.$broadcast('user-invitations-changed');
Notification.success({message: '<i class="glyphicon glyphicon-ok"></i> Signup successful!'});
// And redirect to the previous or home page
$state.go($state.previous.state.name || 'home', $state.previous.params);
}
/**
* onUserSignupError
* @param response
*/
function onUserSignupError(response) {
Notification.error({message: response.data.message, title: '<i class="glyphicon glyphicon-remove"></i> Signup Error!', delay: 6000});
}
/**
* onUserSigninSuccess
* @param response
*/
function onUserSigninSuccess(response) {
// If successful we assign the response to the global user model
vm.authentication.user = response;
$rootScope.$broadcast('auth-user-changed');
$rootScope.$broadcast('user-invitations-changed');
Notification.info({message: 'Welcome ' + response.firstName});
// And redirect to the previous or home page
$state.go($state.previous.state.name || 'home', $state.previous.params);
}
/**
* onUserSigninError
* @param response
*/
function onUserSigninError(response) {
Notification.error({message: response.data.message, title: '<i class="glyphicon glyphicon-remove"></i> Signin Error!', delay: 6000});
}
/**
* markLinkClick
* @param evt

View File

@@ -0,0 +1,16 @@
<div>
<div class="col-sm-10 col-sm-offset-1 margin-top-100 padding-top-20">
<div ng-if="vm.activeMethod === 'invalid'">
<span class="active-notice text-danger" translate="SIGN.ACTIVE_INVALID"></span>
</div>
<div ng-if="vm.activeMethod === 'error'">
<span class="active-notice text-danger" translate="SIGN.ACTIVE_ERROR"></span>
</div>
<div ng-if="vm.activeMethod === 'failed'">
<span class="active-notice text-danger" translate="SIGN.ACTIVE_FAILED"></span>
</div>
<div ng-if="vm.activeMethod === 'successfully'">
<span class="active-notice text-success" translate="SIGN.ACTIVE_SUCCESSFULLY"></span>
</div>
</div>
</div>

View File

@@ -4,74 +4,82 @@
</div>
</div>
<div ng-if="vm.signConfig.openSignup || vm.validToken">
<legend class="col-sm-10 col-sm-offset-1 small-legend margin-top-40">{{ 'SIGN.SIGN_UP' | translate}}</legend>
<div ng-if="!vm.waitToActive">
<legend class="col-sm-10 col-sm-offset-1 small-legend margin-top-40">{{ 'SIGN.SIGN_UP' | translate}}</legend>
<div class="col-sm-8 col-sm-offset-2">
<form name="vm.userForm" ng-submit="vm.signup(vm.userForm.$valid)" class="signin" novalidate autocomplete="off">
<fieldset>
<div class="form-group" show-errors>
<input type="text" id="firstName" name="firstName" class="form-control" ng-model="vm.credentials.firstName"
placeholder="{{ 'STATUS_FIELD.FIRST_NAME' | translate}}" required autofocus>
<div class="col-sm-8 col-sm-offset-2">
<form name="vm.userForm" ng-submit="vm.signup(vm.userForm.$valid)" class="signin" novalidate autocomplete="off">
<fieldset>
<div class="form-group" show-errors>
<input type="text" id="firstName" name="firstName" class="form-control" ng-model="vm.credentials.firstName"
placeholder="{{ 'STATUS_FIELD.FIRST_NAME' | translate}}" required autofocus>
<div ng-messages="vm.userForm.firstName.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.FN_REQUIRED' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="text" id="lastName" name="lastName" class="form-control" ng-model="vm.credentials.lastName"
placeholder="{{ 'STATUS_FIELD.LAST_NAME' | translate}}" required>
<div ng-messages="vm.userForm.lastName.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.LN_REQUIRED' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="email" id="email" name="email" class="form-control" ng-model="vm.credentials.email" lowercase
placeholder="{{ 'STATUS_FIELD.EMAIL' | translate}}" required ng-readonly="vm.emailReadonly">
<div ng-messages="vm.userForm.email.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.E_REQUIRED' | translate}}</p>
<p class="help-block error-text" ng-message="email">{{ 'SIGN.E_INVALID' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="text" id="username" name="username" class="form-control" ng-model="vm.credentials.username"
placeholder="{{ 'STATUS_FIELD.USERNAME' | translate}}" ng-pattern="vm.usernameRegex" lowercase required>
<div ng-messages="vm.userForm.username.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.U_REQUIRED' | translate}}</p>
<p class="help-block error-text"
ng-message="pattern">{{ 'SIGN.U_PATTERN' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="password" id="password" name="password" class="form-control" ng-model="vm.credentials.password"
placeholder="{{ 'SIGN.PASSWORD' | translate}}"
uib-popover="{{vm.getPopoverMsg()}}" popover-trigger="'outsideClick'"
popover-placement="top-right" password-validator required>
<div ng-messages="vm.userForm.password.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.P_REQUIRED' | translate}}</p>
<div ng-repeat="passwordError in passwordErrors">
<p class="help-block error-text" ng-show="vm.userForm.password.$error.requirements">{{passwordError}}</p>
<div ng-messages="vm.userForm.firstName.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.FN_REQUIRED' | translate}}</p>
</div>
</div>
</div>
<div class="form-group" ng-show="!vm.userForm.password.$error.required">
<label>{{ 'SIGN.PASSWORD_REQ' | translate}}</label>
<uib-progressbar value="requirementsProgress" type="{{requirementsColor}}"><span
style="color:white; white-space:nowrap;">{{requirementsProgress}}%</span></uib-progressbar>
</div>
<div class="text-center form-group">
<button type="submit" class="btn btn-primary">{{ 'SIGN.BTN_SIGN_UP' | translate}}</button>
&nbsp; or&nbsp;
<a ui-sref="authentication.signin" class="show-signup">{{ 'SIGN.BTN_SIGN_IN' | translate}}</a>
</div>
</fieldset>
</form>
<div class="form-group" show-errors>
<input type="text" id="lastName" name="lastName" class="form-control" ng-model="vm.credentials.lastName"
placeholder="{{ 'STATUS_FIELD.LAST_NAME' | translate}}" required>
<div ng-messages="vm.userForm.lastName.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.LN_REQUIRED' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="email" id="email" name="email" class="form-control" ng-model="vm.credentials.email" lowercase
placeholder="{{ 'STATUS_FIELD.EMAIL' | translate}}" required ng-readonly="vm.emailReadonly">
<div ng-messages="vm.userForm.email.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.E_REQUIRED' | translate}}</p>
<p class="help-block error-text" ng-message="email">{{ 'SIGN.E_INVALID' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="text" id="username" name="username" class="form-control" ng-model="vm.credentials.username"
placeholder="{{ 'STATUS_FIELD.USERNAME' | translate}}" ng-pattern="vm.usernameRegex" lowercase required>
<div ng-messages="vm.userForm.username.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.U_REQUIRED' | translate}}</p>
<p class="help-block error-text"
ng-message="pattern">{{ 'SIGN.U_PATTERN' | translate}}</p>
</div>
</div>
<div class="form-group" show-errors>
<input type="password" id="password" name="password" class="form-control" ng-model="vm.credentials.password"
placeholder="{{ 'SIGN.PASSWORD' | translate}}"
uib-popover="{{vm.getPopoverMsg()}}" popover-trigger="'outsideClick'"
popover-placement="top-right" password-validator required>
<div ng-messages="vm.userForm.password.$error" role="alert">
<p class="help-block error-text" ng-message="required">{{ 'SIGN.P_REQUIRED' | translate}}</p>
<div ng-repeat="passwordError in passwordErrors">
<p class="help-block error-text" ng-show="vm.userForm.password.$error.requirements">{{passwordError}}</p>
</div>
</div>
</div>
<div class="form-group" ng-show="!vm.userForm.password.$error.required">
<label>{{ 'SIGN.PASSWORD_REQ' | translate}}</label>
<uib-progressbar value="requirementsProgress" type="{{requirementsColor}}"><span
style="color:white; white-space:nowrap;">{{requirementsProgress}}%</span></uib-progressbar>
</div>
<div class="text-center form-group">
<button type="submit" class="btn btn-primary">{{ 'SIGN.BTN_SIGN_UP' | translate}}</button>
&nbsp; or&nbsp;
<a ui-sref="authentication.signin" class="show-signup">{{ 'SIGN.BTN_SIGN_IN' | translate}}</a>
</div>
</fieldset>
</form>
</div>
</div>
<div ng-if="vm.waitToActive">
<div class="col-sm-10 col-sm-offset-1 margin-top-100 padding-top-20">
<span class="active-notice" translate="SERVER.{{vm.waitToActiveTranslate}}"
translate-values="{site: vm.appConfig.name, mail: vm.credentials.email, hours: vm.signConfig.signUpActiveTokenExpires / (60*60*1000)}"></span>
</div>
</div>
</div>
</div>

View File

@@ -10,8 +10,12 @@ var path = require('path'),
passport = require('passport'),
User = mongoose.model('User'),
Invitation = mongoose.model('Invitation'),
nodemailer = require('nodemailer'),
traceLogCreate = require(path.resolve('./config/lib/tracelog')).create;
var smtpTransport = nodemailer.createTransport(config.mailer.options);
var mtConfig = config.meanTorrentConfig;
// URLs for which user can't be redirected on signin
var noReturnUrls = [
'/authentication/signin',
@@ -33,6 +37,9 @@ exports.signup = function (req, res) {
user.displayName = user.firstName + ' ' + user.lastName;
user.passkey = user.randomAsciiString(32);
user.signUpActiveToken = user.randomAsciiString(32);
user.signUpActiveExpires = Date.now() + mtConfig.sign.signUpActiveTokenExpires;
// Then save the user
user.save(function (err) {
if (err) {
@@ -59,14 +66,45 @@ exports.signup = function (req, res) {
user.password = undefined;
user.salt = undefined;
req.login(user, function (err) {
/* send an account active mail */
var httpTransport = 'http://';
if (config.secure && config.secure.ssl === true) {
httpTransport = 'https://';
}
var baseUrl = req.app.get('domain') || httpTransport + req.headers.host;
res.render(path.resolve('modules/users/server/templates/sign-up-active-email'), {
name: user.displayName,
appName: config.app.title,
hours: mtConfig.sign.signUpActiveTokenExpires / (60 * 60 * 1000),
url: baseUrl + '/api/auth/active/' + user.signUpActiveToken
}, function (err, emailHTML) {
if (err) {
res.status(400).send(err);
return res.status(400).send({
message: 'ACTIVE_MAIL_RENDER_ERROR'
});
} else {
res.json(user);
var mailOptions = {
to: user.email,
from: config.mailer.from,
subject: 'Sign up account active of ' + config.app.title,
html: emailHTML
};
smtpTransport.sendMail(mailOptions, function (err) {
if (!err) {
res.send({
message: 'SENDING_ACTIVE_MAIL_SUCCESSFULLY'
});
} else {
return res.status(400).send({
message: 'SENDING_ACTIVE_MAIL_FAILED'
});
}
});
}
});
//create trace log
traceLogCreate(req, traceConfig.action.userSignUp, {
user: user._id,
@@ -77,6 +115,43 @@ exports.signup = function (req, res) {
});
};
/**
* active sign up from email token
*/
exports.active = function (req, res, next) {
User.findOne({
signUpActiveToken: req.params.token,
status: 'inactive',
signUpActiveExpires: {
$gt: Date.now()
}
}, function (err, u) {
if (err || !u) {
return res.redirect('/authentication/active?method=invalid');
} else {
u.update({
$set: {
status: 'normal',
signUpActiveToken: undefined,
signUpActiveExpires: undefined
}
}).exec(function (err) {
if (err) {
return res.redirect('/authentication/active?method=error');
} else {
req.login(u, function (err) {
if (err) {
return res.redirect('/authentication/active?method=failed');
} else {
return res.redirect('/authentication/active?method=successfully');
}
});
}
});
}
});
};
/**
* Signin after passport authentication
*/

View File

@@ -126,7 +126,6 @@ exports.validateResetToken = function (req, res) {
});
};
/**
* invite sign up from email token
*/

View File

@@ -129,7 +129,7 @@ var UserSchema = new Schema({
},
status: {
type: String,
default: 'normal'
default: 'inactive'
},
vip_start_at: {
type: Date,
@@ -213,6 +213,13 @@ var UserSchema = new Schema({
type: Date,
default: Date.now
},
/* for sing up active */
signUpActiveToken: {
type: String
},
signUpActiveExpires: {
type: Date
},
/* For reset password */
resetPasswordToken: {
type: String

View File

@@ -15,6 +15,7 @@ module.exports = function (app) {
app.route('/api/auth/reset/:token').post(users.reset);
app.route('/api/auth/invite/:token').get(users.invite);
app.route('/api/auth/active/:token').get(users.active);
// Setting up the users authentication api
app.route('/api/auth/signup').post(users.signup);

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<p>Dear {{name}},</p>
<br />
<p>
You have requested to sign up and register for your account at {{appName}}
</p>
<p>We should verify your email address, Please visit this url to active your account in {{hours}} hours:</p>
<p>{{url}}</p>
<strong>If you didn't make this request, you can ignore this email.</strong>
<br />
<br />
<p>The {{appName}} Support Team</p>
</body>
</html>