feathers-service-verify-reset
Version:
Adds user email verification and password reset capabilities to local feathers-authentication
948 lines (853 loc) • 34.8 kB
JavaScript
;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/* eslint consistent-return: 0, no-param-reassign: 0, no-underscore-dangle: 0,
no-var: 0, vars-on-top: 0 */
var crypto = require('crypto');
var errors = require('feathers-errors');
var auth = require('feathers-authentication').hooks;
var bcrypt = require('bcryptjs');
var hooks = require('feathers-hooks-common');
var utils = require('feathers-hooks-common/lib/utils');
var debug = require('debug')('verify-reset');
var options = {
userNotifier: function userNotifier(p1, p2, p3, p4, cb) {
cb(null);
},
longTokenLen: 15, // token's length will be twice this
shortTokenLen: 6,
shortTokenDigits: true,
resetDelay: 1000 * 60 * 60 * 2, // 2 hours
delay: 1000 * 60 * 60 * 24 * 5, // 5 days
userPropsForShortToken: ['email']
};
/**
* Feathers-service-verify-reset service to verify user's email, and to reset forgotten password.
*
* @param {Object?} options1 - for service
*
* options1.userNotifier - function(type, user, notifierOptions, newEmail, cb)
* type type of notification
* 'resendVerifySignup' from resendVerifySignup API call
* 'verifySignup' from verifySignupLong and verifySignupShort API calls
* 'sendResetPwd' from sendResetPwd API call
* 'resetPwd' from resetPwdLong and resetPwdShort API calls
* 'passwordChange' from passwordChange API call
* 'emailChange' from emailChange API call
* user user's item, minus password.
* notifierOptions notifierOptions option from resendVerifySignup and sendResetPwd API calls
* newEmail the new email address from emailChange API call
*
* userNotifier needs to handle at least the resendVerifySignup and sendResetPwd notifications.
*
* options1.longTokenLen
* - Half the length of the long token. Default is 15, giving 30-char tokens.
* options1.shortTokenLen
* - Length of short token. Default is 6.
* options1.shortTokenDigits
* - Short token is digits if true, else alphanumeric. Default is true.
* options1.delay
* - Duration for sign up email verification token in ms. Default is 5 days.
* options1.resetDelay
* - Duration for password reset token in ms. Default is 2 hours.
* options1.userPropsForShortToken
* - A 6-digit short token is more susceptible to brute force attack than a 30-char token.
* Therefore the verifySignupShort and resetPwdShort API calls require the user be identified
* using a find-query-like object. To prevent this itself from being an attack vector,
* userPropsForShortToken is an array of valid properties allowed in that query object.
* The default is ['email']. You may change it to ['email', 'username'] if you want to
* identify users by {email} or {username} or {email, username}.
*
* @returns {Function} Featherjs service
*
* (A) THE SERVICE
* The service creates and maintains the following properties in the user item:
* isVerified if the user's email addr has been verified (boolean)
* verifyToken the 30-char token generated for email addr verification (string)
* verifyTokenShort the 6-digit token generated for email addr verification (string)
* verifyExpires when the email addr token expire (Date)
* resetToken the 30-char token generated for forgotten password reset (string)
* resetTokenShort the 6-digit token generated for forgotten password reset (string)
* resetExpires when the forgotten password token expire (Date)
*
* The service is configured on the server with
* app.configure(authentication)
* .configure(verifyReset({ userNotifier }))
* .configure(user);
*
* It may be called on the client using
* - (A1) Feathers method calls,
* - (A2) provided service wrappers,
* - (A3) HTTP fetch **(docs todo)**
* - (A4) React's Redux
* - (A5) Vue 2.0 **(docs todo)**
*
* **(A1) USING FEATHERS' METHOD CALLS**
* Method calls return a Promise unless a callback is provided.
*
* const verifyReset = app.service('/verifyReset/:action/:value');
* verifyReset.create({ action, value, ... [, cb]});
*
* // check props are unique in the users items
* verifyReset.create({ action: 'checkUnique',
* value: uniques, // e.g. {email, username}. Props with null or undefined are ignored.
* ownId, // excludes your current user from the search
* meta: { noErrMsg }, // if return an error.message if not unique
* }, {}, cb)
*
* // resend email verification notification
* verifyReset.create({ action: 'resendVerifySignup',
* value: emailOrToken, // email, {email}, {token}
* notifierOptions: {}, // options passed to options1.userNotifier, e.g. {transport: 'sms'}
* }, {}, cb)
*
* // email addr verification with long token
* verifyReset.create({ action: 'verifySignupLong',
* value: token, // compares to .verifyToken
* }, {}, cb)
*
* // email addr verification with short token
* verifyReset.create({ action: 'verifySignupShort',
* value: {
* token, // compares to .verifyTokenShort
* user: {} // identify user, e.g. {email: 'a@a.com'}. See options1.userPropsForShortToken.
* }
* }, {}, cb)
*
* // send forgotten password notification
* verifyReset.create({ action: 'sendResetPwd',
* value: email,
* notifierOptions: {}, // options passed to options1.userNotifier, e.g. {transport: 'sms'}
* }, {}, cb)
*
* // forgotten password verification with long token
* verifyReset.create({ action: 'resetPwdLong',
* value: {
* token, // compares to .resetToken
* password, // new password
* },
* }, {}, cb)
*
* // forgotten password verification with short token
* verifyReset.create({ action: 'resetPwdShort',
* value: {
* token, // compares to .resetTokenShort
* password, // new password
* user: {} // identify user, e.g. {email: 'a@a.com'}. See options1.userPropsForShortToken.
* },
* }, {}, cb)
*
* // change password
* verifyReset.create({ action: 'passwordChange',
* value: {
* oldPassword, // old password for verification
* password, // new password
* },
* }, { user }, cb)
*
* // change email
* verifyReset.create({ action: 'emailChange',
* value: {
* password, // current password for verification
* email, // new email
* },
* }, { user }, cb)
*
* // Authenticate user and log on if user is verified.
* var cbCalled = false;
* app.authenticate({ type: 'local', email, password })
* .then((result) => {
* const user = result.data;
* if (!user || !user.isVerified) {
* app.logout();
* cb(new Error(user ? 'User\'s email is not verified.' : 'No user returned.'));
* return;
* }
* cbCalled = true;
* cb(null, user);
* })
* .catch((err) => {
* if (!cbCalled) { cb(err); } // ignore throws from .then( cb(null, user) )
* });
*
*
* **(A2) PROVIDED SERVICE WRAPPERS**
* The wrappers return a Promise unless a callback is provided.
* See example/ for a working example of wrapper usage.
*
* <script src=".../feathers-service-verify-reset/lib/client.js"></script>
* or
* import VerifyRest from 'feathers-service-verify-reset/lib/client';
*
* const app = feathers() ...
* const verifyReset = new VerifyReset(app);
*
* // check props are unique in the users items
* verifyReset.checkUnique(uniques, ownId, ifErrMsg, cb)
*
* // resend email verification notification
* verifyReset.resendVerifySignup(emailOrToken, notifierOptions, cb)
*
* // email addr verification with long token
* verifyReset.verifySignupLong(token, cb)
*
* // email addr verification with short token
* verifyReset.verifySignupShort(token, userFind, cb)
*
* // send forgotten password notification
* verifyReset.sendResetPwd(email, notifierOptions, cb)
*
* // forgotten password verification with long token
* verifyReset.resetPwdLong(token, password, cb)
*
* // forgotten password verification with short token
* verifyReset.resetPwdShort(token, userFind, password, cb)
*
* // change password
* verifyReset.passwordChange(oldPassword, password, user, cb)
*
* // change email
* verifyReset.emailChange(password, email, user, cb)
*
* // Authenticate user and log on if user is verified.
* verifyReset.authenticate(email, password, cb)
*
*
* **(A3) HTTP FETCH (docs todo)**
* // check props are unique in the users items
* // Set params just like (A1).
* fetch('/verifyReset/:action/:value', {
* method: 'POST', headers: { Accept: 'application/json' },
* body: JSON.stringify({ action: 'checkUnique', value: uniques, ownId, meta: { noErrMsg } })
* })
* .then(data => { ... }).catch(err => { ... });
*
*
* **(A4) REACT'S REDUX**
* See feathers-reduxify-services for information about state, etc.
* See feathers-starter-react-redux-login-roles for a working example.
*
* (A4.1) Dispatching services
* import feathers from 'feathers-client';
* import reduxifyServices, { getServicesStatus } from 'feathers-reduxify-services';
* const app = feathers().configure(feathers.socketio(socket)).configure(feathers.hooks());
* const services = reduxifyServices(app, ['users', 'verifyReset', ...]);
* ...
* // hook up Redux reducers
* export default combineReducers({
* users: services.users.reducer,
* verifyReset: services.verifyReset.reducer,
* });
* ...
*
* // email addr verification with long token
* // Feathers is now 100% compatible with Redux. Use just like (A1).
* store.dispatch(services.verifyReset.create({ action: 'verifySignupLong',
* value: token, // compares to .verifyToken
* }, {})
* );
*
* (A4.2) Dispatching authentication. User must be verified to sign in.
* const reduxifyAuthentication = require('feathers-reduxify-authentication');
* const signin = reduxifyAuthentication(app, { isUserAuthorized: (user) => user.isVerified });
*
* // Sign in with the JWT currently in localStorage
* if (localStorage['feathers-jwt']) {
* store.dispatch(signin.authenticate()).catch(err => { ... });
* }
*
* // Sign in with credentials
* store.dispatch(signin.authenticate({ type: 'local', email, password }))
* .then(() => { ... )
* .catch(err => { ... });
*
*
* **(A5) VUE 2.0 (docs todo)**
*
*
* (B) HOOKS
* This service itself does not handle creation of a new user account nor the sending of the initial
* email verification request.
* Instead hooks are provided for you to use with the 'users' service 'create' method.
*
* const verifyHooks = require('feathers-service-verify-reset').verifyResetHooks;
* export.before = {
* create: [
* auth.hashPassword(),
* verifyHooks.addVerification() // adds .isVerified, .verifyExpires, .verifyToken props
* ]
* };
* export.after = {
* create: [
* hooks.remove('password'),
* aHookToEmailYourVerification(),
* verifyHooks.removeVerification() // removes verification/reset fields other than .isVerified
* ]
* };
*
*
* A hook is provided to ensure the user's email addr is verified:
*
* const auth = require('feathers-authentication').hooks;
* const verify = require('feathers-service-verify-reset').hooks;
* export.before = {
* create: [
* auth.verifyToken(),
* auth.populateUser(),
* auth.restrictToAuthenticated(),
* verify.restrictToVerified()
* ]
* };
*
*
* (C) SECURITY
* - The user must be identified when the short token is used, making the short token less appealing
* as an attack vector.
* - The long and short tokens are erased on successful verification and password reset attempts.
* New tokens must be acquired for another attempt.
* - API params are verified to be strings. If the param is an object, the values of its props are
* verified to be strings.
* - options1.userPropsForShortToken restricts the prop names allowed in param objects.
*
* (D) CONFIGURABLE:
* The length of the "30-char" token is configurable.
* The length of the "6-digit" token is configurable. It may also be configured as alphanumeric.
*
* (E) MIGRATION FROM 0.8.0
* A few changes are needed to migrate to 1.0.0. Names were standardized throughout the
* new version and these had to be changed.
*
* options1.userNotifier signature
* was (type, user1, params, cb)
* now (type, user1, notifierOptions, newEmail, cb)
* options1.userNotifier param 'type'
* 'resend' now 'resendVerifySignup'
* 'verify' now 'verifySignup'
* 'forgot' now 'sendResetPwd'
* 'reset' now 'resetPwd'
* 'password' now 'passwordChange'
* 'email' now 'emailChange'
*
* Error messages used to return
* new errors.BadRequest('Password is incorrect.',
* { errors: { password: 'Password is incorrect.' } })
* This was hacky although convenient if the names matched your UI. Now they return
* new errors.BadRequest('Password is incorrect.',
* { errors: { password: 'Password is incorrect.', $className: 'badParams' } })
* or even
* new errors.BadRequest('Password is incorrect.',
* { errors: { $className: 'badParams' } })
* Set your local UI errors based on the $className value.
*
* The following are deprecated but remain working. They will be removed in the future.
* options1
* emailer uses options1.userNotifier
* API param 'action'
* 'unique' uses 'checkUnique'
* 'resend' uses 'resendVerifySignup'
* 'verify' uses 'verifySignupLong'
* 'forgot' uses 'sendResetPwd'
* 'reset' uses 'resetPwdLong'
* 'password' uses 'passwordChange'
* 'email' uses 'emailChange'
* client wrapper
* .unique uses .checkUnique
* .verifySignUp uses .verifySignupLong
* .sendResetPassword uses .sendResetPwd
* .saveResetPassword uses .resetPwdLong
* .changePassword uses .passwordChange
* .changeEmail uses .emailChange
*
* The service now uses the route /verifyreset rather than /verifyReset/:action/:value
*
*/
module.exports.service = function () {
var options1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
debug('service configured.');
options = Object.assign(options, options1, options1.emailer ? { userNotifier: options1.emailer } : {});
return function verifyReset() {
// 'function' needed as we use 'this'
debug('service initialized');
var app = this;
var path = 'verifyReset';
var users;
var params;
var isAction = function isAction() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return function (hook) {
return args.indexOf(hook.data.action) !== -1;
};
};
app.use(path, {
before: {
create: [hooks.iff(isAction('password', 'email'), auth.verifyToken()), hooks.iff(isAction('password', 'email'), auth.populateUser())]
},
create: function create(data, params1, cb) {
debug('service called. action=' + data.action);
var callbackCalled = false;
var promise;
users = app.service('/users'); // here in case users service is configured after verifyReset
params = params1;
switch (data.action) {
case 'unique': // backwards compatible, fall through
case 'checkUnique':
promise = checkUniqueness(data.value, data.ownId || null, data.meta || {});
break;
case 'resend': // backwards compatible, fall through
case 'resendVerifySignup':
promise = resendVerifySignup(data.value, data.notifierOptions);
break;
case 'verify': // backwards compatible, fall through
case 'verifySignupLong':
promise = verifySignupWithLongToken(data.value);
break;
case 'verifySignupShort':
promise = verifySignupWithShortToken(data.value.token, data.value.user);
break;
case 'forgot': // backwards compatible, fall through
case 'sendResetPwd':
promise = sendResetPwd(data.value, data.notifierOptions);
break;
case 'reset': // backwards compatible, fall through
case 'resetPwdLong':
promise = resetPwdWithLongToken(data.value.token, data.value.password);
break;
case 'resetPwdShort':
promise = resetPwdWithShortToken(data.value.token, data.value.user, data.value.password);
break;
case 'password': // backwards compatible, fall through
case 'passwordChange':
promise = passwordChange(params.user, data.value.oldPassword, data.value.password);
break;
case 'email': // backwards compatible, fall through
case 'emailChange':
promise = emailChange(params.user, data.value.password, data.value.email);
break;
default:
promise = Promise.reject(new errors.BadRequest('Action \'' + data.action + '\' is invalid.', { errors: { $className: 'badParams' } }));
}
if (cb) {
// Feathers cannot return an error to the client if we use process.nextTick(cb, err)
promise.then(function (data1) {
callbackCalled = true;
cb(null, data1);
}).catch(function (err) {
if (!callbackCalled) {
// don't call cb should cb(null, data) throw
callbackCalled = true;
cb(err);
}
});
}
return promise;
}
});
function checkUniqueness(uniques, ownId, meta) {
var keys = Object.keys(uniques).filter(function (key) {
return uniques[key] !== undefined && uniques[key] !== null;
});
return Promise.all(keys.map(function (prop) {
return users.find({ query: _defineProperty({}, prop, uniques[prop].trim()) }).then(function (data) {
var items = Array.isArray(data) ? data : data.data;
var isNotUnique = items.length > 1 || items.length === 1 && (items[0].id || items[0]._id) !== ownId;
return isNotUnique ? prop : null;
});
})).catch(function (err) {
throw new errors.GeneralError(err);
}).then(function (allProps) {
var errProps = allProps.filter(function (prop) {
return prop;
});
if (errProps.length) {
(function () {
var errs = {};
errProps.forEach(function (prop) {
errs[prop] = 'Already taken.';
});
throw new errors.BadRequest(meta.noErrMsg ? null : 'Values already taken.', { errors: errs });
})();
}
});
}
// email, {email}, {verifyToken}, {verifyShortToken},
// {email, verifyToken, verifyShortToken}
function resendVerifySignup(emailOrToken, notifierOptions) {
debug('resendVerifySignup', emailOrToken);
return Promise.resolve().then(function () {
if (typeof emailOrToken === 'string') {
// backwards compatibility
emailOrToken = { email: emailOrToken };
}
ensureObjPropsValid(emailOrToken, ['email', 'verifyToken', 'verifyShortToken']);
return emailOrToken;
}).then(function (query) {
return Promise.all([users.find({ query: query }).then(function (data) {
return getUserData(data, ['isNotVerified']);
}), getLongToken(options.longTokenLen), getShortToken(options.shortTokenLen, options.shortTokenDigits)]);
}).then(function (_ref) {
var _ref2 = _slicedToArray(_ref, 3),
user = _ref2[0],
longToken = _ref2[1],
shortToken = _ref2[2];
return patchUser(user, {
isVerified: false,
verifyExpires: Date.now() + options.delay,
verifyToken: longToken,
verifyShortToken: shortToken
});
}).then(function (user) {
return userNotifier('resendVerifySignup', user, notifierOptions);
}).then(function (user) {
return sanitizeUserForClient(user);
});
}
function verifySignupWithLongToken(verifyToken) {
return Promise.resolve().then(function () {
ensureValuesAreStrings(verifyToken);
return verifySignup({ verifyToken: verifyToken }, { verifyToken: verifyToken });
});
}
function verifySignupWithShortToken(verifyShortToken, findUser) {
return Promise.resolve().then(function () {
ensureValuesAreStrings(verifyShortToken);
ensureObjPropsValid(findUser, options.userPropsForShortToken);
return verifySignup(findUser, { verifyShortToken: verifyShortToken });
});
}
function verifySignup(query, tokens) {
return users.find({ query: query }).then(function (data) {
return getUserData(data, ['isNotVerified', 'verifyNotExpired']);
}).then(function (user) {
if (!Object.keys(tokens).every(function (key) {
return tokens[key] === user[key];
})) {
return patchUser(user, {
verifyToken: null,
verifyShortToken: null,
verifyExpires: null
}).then(function () {
throw new errors.BadRequest('Invalid token. Get for a new one. (verify-reset)', { errors: { $className: 'badParam' } });
});
}
return patchUser(user, {
isVerified: user.verifyExpires > Date.now(),
verifyToken: null,
verifyShortToken: null,
verifyExpires: null
}).then(function (user1) {
return userNotifier('verifySignup', user1);
}).then(function (user1) {
return sanitizeUserForClient(user1);
});
});
}
function sendResetPwd(email, notifierOptions) {
debug('sendResetPwd');
return Promise.resolve().then(function () {
ensureValuesAreStrings(email);
return Promise.all([users.find({ query: { email: email } }).then(function (data) {
return getUserData(data, ['isVerified']);
}), getLongToken(options.longTokenLen), getShortToken(options.shortTokenLen, options.shortTokenDigits)]);
}).then(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 3),
user = _ref4[0],
longToken = _ref4[1],
shortToken = _ref4[2];
return patchUser(user, {
resetExpires: Date.now() + options.resetDelay,
resetToken: longToken,
resetShortToken: shortToken
});
}).then(function (user) {
return userNotifier('sendResetPwd', user, notifierOptions);
}).then(function (user) {
return sanitizeUserForClient(user);
});
}
function resetPwdWithLongToken(resetToken, password) {
return Promise.resolve().then(function () {
ensureValuesAreStrings(resetToken, password);
return resetPassword({ resetToken: resetToken }, { resetToken: resetToken }, password);
});
}
function resetPwdWithShortToken(resetShortToken, findUser, password) {
return Promise.resolve().then(function () {
ensureValuesAreStrings(resetShortToken, password);
ensureObjPropsValid(findUser, options.userPropsForShortToken);
return resetPassword(findUser, { resetShortToken: resetShortToken }, password);
});
}
function resetPassword(query, tokens, password) {
return Promise.all([users.find({ query: query }).then(function (data) {
return getUserData(data, ['isVerified', 'resetNotExpired']);
}), hashPassword(app, password)]).then(function (_ref5) {
var _ref6 = _slicedToArray(_ref5, 2),
user = _ref6[0],
hashedPassword = _ref6[1];
if (!Object.keys(tokens).every(function (key) {
return tokens[key] === user[key];
})) {
return patchUser(user, {
resetToken: null,
resetShortToken: null,
resetExpires: null
}).then(function () {
throw new errors.BadRequest('Invalid token. Get for a new one. (verify-reset)', { errors: { $className: 'badParam' } });
});
}
return patchUser(user, {
password: hashedPassword,
resetToken: null,
resetShortToken: null,
resetExpires: null
}).then(function (user1) {
return userNotifier('resetPwd', user1);
}).then(function (user1) {
return sanitizeUserForClient(user1);
});
});
}
function passwordChange(user, oldPassword, password) {
debug('passwordChange', oldPassword, password);
return Promise.resolve().then(function () {
ensureValuesAreStrings(oldPassword, password);
return users.find({ query: { email: user.email } }).then(function (data) {
return Array.isArray(data) ? data[0] : data.data[0];
});
}).then(function (user1) {
return Promise.all([user1, hashPassword(app, password), comparePasswords(oldPassword, user1.password, function () {
return new errors.BadRequest('Current password is incorrect.', { errors: { oldPassword: 'Current password is incorrect.' } });
})]);
}).then(function (_ref7) {
var _ref8 = _slicedToArray(_ref7, 2),
user1 = _ref8[0],
hashedPassword = _ref8[1];
return (// value from comparePassword is not needed
patchUser(user1, {
password: hashedPassword
})
);
}).then(function (user1) {
return userNotifier('passwordChange', user1);
}).then(function (user1) {
return sanitizeUserForClient(user1);
});
}
function emailChange(user, password, email) {
// note this call does not update the authenticated user info in hooks.params.user.
debug('emailChange', password, email);
var idType = user._id ? '_id' : 'id';
return Promise.resolve().then(function () {
ensureValuesAreStrings(password, email);
return users.find({ query: _defineProperty({}, idType, user[idType]) }).then(function (data) {
return Array.isArray(data) ? data[0] : data.data[0];
});
}).then(function (user1) {
return Promise.all([user1, comparePasswords(password, user1.password, function () {
return new errors.BadRequest('Password is incorrect.', { errors: { password: 'Password is incorrect.', $className: 'badParams' } });
})]);
}).then(function (_ref9) {
var _ref10 = _slicedToArray(_ref9, 1),
user1 = _ref10[0];
return userNotifier('emailChange', user1, null, email);
}) // value from comparePassword not need
.then(function (user1) {
return patchUser(user1, { email: email });
}).then(function (user1) {
return sanitizeUserForClient(user1);
});
}
// Helpers requiring this closure
function patchUser(user, patchToUser) {
return users.patch(user.id || user._id, patchToUser, {}) // needs users from closure
.then(function () {
return Object.assign(user, patchToUser);
});
}
};
};
// Hooks
module.exports.hooks = {};
module.exports.hooks.addVerification = function (options1) {
return function (hook) {
utils.checkContext(hook, 'before', 'create');
var ourOptions = Object.assign({}, options, options1);
if (options1 && options1.len) {
ourOptions.longTokenLen = options1.len; // backward compatibility
}
return Promise.all([getLongToken(ourOptions.longTokenLen), getShortToken(ourOptions.shortTokenLen, ourOptions.shortTokenDigits)]).then(function (_ref11) {
var _ref12 = _slicedToArray(_ref11, 2),
longToken = _ref12[0],
shortToken = _ref12[1];
hook.data.isVerified = false;
hook.data.verifyExpires = Date.now() + ourOptions.delay;
hook.data.verifyToken = longToken;
hook.data.verifyShortToken = shortToken;
return hook;
}).catch(function (err) {
throw new errors.GeneralError(err);
});
};
};
module.exports.hooks.restrictToVerified = function () {
return function (hook) {
utils.checkContext(hook, 'before');
if (!hook.params.user || !hook.params.user.isVerified) {
throw new errors.BadRequest('User\'s email is not yet verified.');
}
};
};
module.exports.hooks.removeVerification = function (ifReturnTokens) {
return function (hook) {
utils.checkContext(hook, 'after');
var user = hook.result || {};
if (!('isVerified' in user) && hook.method === 'create') {
/* eslint-disable no-console */
console.warn('Property isVerified not found in user properties. (removeVerification)');
console.warn('Have you added verify-reset\'s properties to your model? (Refer to README.md)');
console.warn('Have you added the addVerification hook on users::create?');
/* eslint-enable */
}
if (hook.params.provider && user) {
// noop if initiated by server
delete user.verifyExpires;
delete user.resetExpires;
if (!ifReturnTokens) {
delete user.verifyToken;
delete user.verifyShortToken;
delete user.resetToken;
delete user.resetShortToken;
}
}
};
};
// Helpers
function hashPassword(app1, password) {
var hook = {
type: 'before',
data: { password: password },
params: { provider: null },
app: {
get: function get(str) {
return app1.get(str);
}
}
};
return auth.hashPassword()(hook).then(function (hook1) {
return hook1.data.password;
});
}
function comparePasswords(oldPassword, password, getError) {
return new Promise(function (resolve, reject) {
bcrypt.compare(oldPassword, password, function (err, data1) {
if (err || !data1) {
return reject(getError());
}
return resolve();
});
});
}
function getLongToken(len) {
return randomBytes(len || options.longTokenLen);
}
function getShortToken(len, ifDigits) {
len = len || options.shortTokenLen;
if (ifDigits) {
return Promise.resolve(randomDigits(len));
}
return randomBytes(Math.floor(len / 2) + 1).then(function (str) {
str = str.substr(0, len);
if (str.match(/^[0-9]+$/)) {
// tests will fail on all digits
str = 'q' + str.substr(1); // shhhh, secret.
}
return str;
});
}
function randomBytes(len) {
return new Promise(function (resolve, reject) {
crypto.randomBytes(len, function (err, buf) {
return err ? reject(err) : resolve(buf.toString('hex'));
});
});
}
module.exports.randomBytes = function () {
return randomBytes.apply(undefined, arguments);
}; // made safe for testing
function randomDigits(len) {
var str = Math.random().toString() + Array(len + 1).join('0');
return str.substr(2, len);
}
module.exports.randomDigits = function () {
return randomDigits.apply(undefined, arguments);
}; // made safe for testing
function getUserData(data, checks) {
if (Array.isArray(data) ? data.length === 0 : data.total === 0) {
throw new errors.BadRequest('User not found.', { errors: { $className: 'badParams' } });
}
var users = Array.isArray(data) ? data : data.data;
var user = users[0];
if (users.length !== 1) {
throw new errors.BadRequest('More than 1 user selected.', { errors: { $className: 'badParams' } });
}
if (checks.indexOf('isNotVerified') !== -1 && user.isVerified) {
throw new errors.BadRequest('User is already verified.', { errors: { $className: 'isNotVerified' } });
}
if (checks.indexOf('isVerified') !== -1 && !user.isVerified) {
throw new errors.BadRequest('User is not verified.', { errors: { $className: 'isVerified' } });
}
if (checks.indexOf('verifyNotExpired') !== -1 && user.verifyExpires < Date.now()) {
throw new errors.BadRequest('Verification token has expired.', { errors: { $className: 'verifyExpired' } });
}
if (checks.indexOf('resetNotExpired') !== -1 && user.resetExpires < Date.now()) {
throw new errors.BadRequest('Password reset token has expired.', { errors: { $className: 'resetExpired' } });
}
return user;
}
function userNotifier(type, user, notifierOptions, newEmail) {
debug('userNotifier', type);
var user1 = Object.assign({}, user, newEmail ? { newEmail: newEmail } : {});
return new Promise(function (resolve, reject) {
options.userNotifier(type, sanitizeUserForEmail(user1), notifierOptions || {}, newEmail || '', function (err2) {
debug(type + '. Completed.');
return err2 ? reject(err2) : resolve(user);
});
});
}
function ensureObjPropsValid(obj, props, allowNone) {
var keys = Object.keys(obj);
var valid = keys.every(function (key) {
return props.indexOf(key) !== -1 && typeof obj[key] === 'string';
});
if (!valid || keys.length === 0 && !allowNone) {
throw new errors.BadRequest('User info is not valid. (verify-reset)', { errors: { $className: 'badParams' } });
}
}
function ensureValuesAreStrings() {
for (var _len2 = arguments.length, rest = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
rest[_key2] = arguments[_key2];
}
if (!rest.every(function (str) {
return typeof str === 'string';
})) {
throw new errors.BadRequest('Expected string value. (verify-reset)', { errors: { $className: 'badParams' } });
}
}
function sanitizeUserForClient(user) {
var user1 = Object.assign({}, user);
delete user1.password;
delete user1.verifyExpires;
delete user1.verifyToken;
delete user1.verifyShortToken;
delete user1.resetExpires;
delete user1.resetToken;
delete user1.resetShortToken;
return user1;
}
function sanitizeUserForEmail(user) {
var user1 = Object.assign({}, user);
delete user1.password;
return user1;
}