|
|
|
@ -1,6 +1,5 @@
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var async = require('async');
|
|
|
|
|
var nconf = require('nconf');
|
|
|
|
|
var winston = require('winston');
|
|
|
|
|
|
|
|
|
@ -16,185 +15,119 @@ var UserReset = module.exports;
|
|
|
|
|
|
|
|
|
|
var twoHours = 7200000;
|
|
|
|
|
|
|
|
|
|
UserReset.validate = function (code, callback) {
|
|
|
|
|
async.waterfall([
|
|
|
|
|
function (next) {
|
|
|
|
|
db.getObjectField('reset:uid', code, next);
|
|
|
|
|
},
|
|
|
|
|
function (uid, next) {
|
|
|
|
|
UserReset.validate = async function (code) {
|
|
|
|
|
const uid = await db.getObjectField('reset:uid', code);
|
|
|
|
|
if (!uid) {
|
|
|
|
|
return callback(null, false);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
db.sortedSetScore('reset:issueDate', code, next);
|
|
|
|
|
},
|
|
|
|
|
function (issueDate, next) {
|
|
|
|
|
next(null, parseInt(issueDate, 10) > Date.now() - twoHours);
|
|
|
|
|
},
|
|
|
|
|
], callback);
|
|
|
|
|
const issueDate = await db.sortedSetScore('reset:issueDate', code);
|
|
|
|
|
return parseInt(issueDate, 10) > Date.now() - twoHours;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UserReset.generate = function (uid, callback) {
|
|
|
|
|
var code = utils.generateUUID();
|
|
|
|
|
async.parallel([
|
|
|
|
|
async.apply(db.setObjectField, 'reset:uid', code, uid),
|
|
|
|
|
async.apply(db.sortedSetAdd, 'reset:issueDate', Date.now(), code),
|
|
|
|
|
], function (err) {
|
|
|
|
|
callback(err, code);
|
|
|
|
|
});
|
|
|
|
|
UserReset.generate = async function (uid) {
|
|
|
|
|
const code = utils.generateUUID();
|
|
|
|
|
await Promise.all([
|
|
|
|
|
db.setObjectField('reset:uid', code, uid),
|
|
|
|
|
db.sortedSetAdd('reset:issueDate', Date.now(), code),
|
|
|
|
|
]);
|
|
|
|
|
return code;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function canGenerate(uid, callback) {
|
|
|
|
|
async.waterfall([
|
|
|
|
|
function (next) {
|
|
|
|
|
db.sortedSetScore('reset:issueDate:uid', uid, next);
|
|
|
|
|
},
|
|
|
|
|
function (score, next) {
|
|
|
|
|
async function canGenerate(uid) {
|
|
|
|
|
const score = await db.sortedSetScore('reset:issueDate:uid', uid);
|
|
|
|
|
if (score > Date.now() - (1000 * 60)) {
|
|
|
|
|
return next(new Error('[[error:reset-rate-limited]]'));
|
|
|
|
|
throw new Error('[[error:reset-rate-limited]]');
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
},
|
|
|
|
|
], callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UserReset.send = function (email, callback) {
|
|
|
|
|
var uid;
|
|
|
|
|
async.waterfall([
|
|
|
|
|
function (next) {
|
|
|
|
|
user.getUidByEmail(email, next);
|
|
|
|
|
},
|
|
|
|
|
function (_uid, next) {
|
|
|
|
|
if (!_uid) {
|
|
|
|
|
return next(new Error('[[error:invalid-email]]'));
|
|
|
|
|
UserReset.send = async function (email) {
|
|
|
|
|
const uid = await user.getUidByEmail(email);
|
|
|
|
|
if (!uid) {
|
|
|
|
|
throw new Error('[[error:invalid-email]]');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uid = _uid;
|
|
|
|
|
canGenerate(uid, next);
|
|
|
|
|
},
|
|
|
|
|
function (next) {
|
|
|
|
|
db.sortedSetAdd('reset:issueDate:uid', Date.now(), uid, next);
|
|
|
|
|
},
|
|
|
|
|
function (next) {
|
|
|
|
|
UserReset.generate(uid, next);
|
|
|
|
|
},
|
|
|
|
|
function (code, next) {
|
|
|
|
|
emailer.send('reset', uid, {
|
|
|
|
|
await canGenerate(uid);
|
|
|
|
|
await db.sortedSetAdd('reset:issueDate:uid', Date.now(), uid);
|
|
|
|
|
const code = await UserReset.generate(uid);
|
|
|
|
|
await emailer.send('reset', uid, {
|
|
|
|
|
reset_link: nconf.get('url') + '/reset/' + code,
|
|
|
|
|
subject: '[[email:password-reset-requested]]',
|
|
|
|
|
template: 'reset',
|
|
|
|
|
uid: uid,
|
|
|
|
|
}, next);
|
|
|
|
|
},
|
|
|
|
|
], callback);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UserReset.commit = function (code, password, callback) {
|
|
|
|
|
var uid;
|
|
|
|
|
async.waterfall([
|
|
|
|
|
function (next) {
|
|
|
|
|
user.isPasswordValid(password, next);
|
|
|
|
|
},
|
|
|
|
|
function (next) {
|
|
|
|
|
UserReset.validate(code, next);
|
|
|
|
|
},
|
|
|
|
|
function (validated, next) {
|
|
|
|
|
UserReset.commit = async function (code, password) {
|
|
|
|
|
await user.isPasswordValid(password);
|
|
|
|
|
const validated = await UserReset.validate(code);
|
|
|
|
|
if (!validated) {
|
|
|
|
|
return next(new Error('[[error:reset-code-not-valid]]'));
|
|
|
|
|
throw new Error('[[error:reset-code-not-valid]]');
|
|
|
|
|
}
|
|
|
|
|
db.getObjectField('reset:uid', code, next);
|
|
|
|
|
},
|
|
|
|
|
function (_uid, next) {
|
|
|
|
|
uid = _uid;
|
|
|
|
|
const uid = await db.getObjectField('reset:uid', code);
|
|
|
|
|
if (!uid) {
|
|
|
|
|
return next(new Error('[[error:reset-code-not-valid]]'));
|
|
|
|
|
throw new Error('[[error:reset-code-not-valid]]');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user.hashPassword(password, next);
|
|
|
|
|
},
|
|
|
|
|
function (hash, next) {
|
|
|
|
|
async.series([
|
|
|
|
|
async.apply(user.setUserFields, uid, { password: hash, 'email:confirmed': 1 }),
|
|
|
|
|
async.apply(db.deleteObjectField, 'reset:uid', code),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'reset:issueDate', code),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'reset:issueDate:uid', uid),
|
|
|
|
|
async.apply(user.reset.updateExpiry, uid),
|
|
|
|
|
async.apply(user.auth.resetLockout, uid),
|
|
|
|
|
async.apply(db.delete, 'uid:' + uid + ':confirm:email:sent'),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'users:notvalidated', uid),
|
|
|
|
|
async.apply(UserReset.cleanByUid, uid),
|
|
|
|
|
], function (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
], callback);
|
|
|
|
|
const hash = await user.hashPassword(password);
|
|
|
|
|
|
|
|
|
|
await user.setUserFields(uid, { password: hash, 'email:confirmed': 1 });
|
|
|
|
|
await db.deleteObjectField('reset:uid', code);
|
|
|
|
|
await db.sortedSetRemoveBulk([
|
|
|
|
|
['reset:issueDate', code],
|
|
|
|
|
['reset:issueDate:uid', uid],
|
|
|
|
|
['users:notvalidated', uid],
|
|
|
|
|
]);
|
|
|
|
|
await user.reset.updateExpiry(uid);
|
|
|
|
|
await user.auth.resetLockout(uid);
|
|
|
|
|
await db.delete('uid:' + uid + ':confirm:email:sent');
|
|
|
|
|
await UserReset.cleanByUid(uid);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UserReset.updateExpiry = function (uid, callback) {
|
|
|
|
|
var oneDay = 1000 * 60 * 60 * 24;
|
|
|
|
|
var expireDays = meta.config.passwordExpiryDays;
|
|
|
|
|
var expiry = Date.now() + (oneDay * expireDays);
|
|
|
|
|
|
|
|
|
|
callback = callback || function () {};
|
|
|
|
|
user.setUserField(uid, 'passwordExpiry', expireDays > 0 ? expiry : 0, callback);
|
|
|
|
|
UserReset.updateExpiry = async function (uid) {
|
|
|
|
|
const oneDay = 1000 * 60 * 60 * 24;
|
|
|
|
|
const expireDays = meta.config.passwordExpiryDays;
|
|
|
|
|
const expiry = Date.now() + (oneDay * expireDays);
|
|
|
|
|
await user.setUserField(uid, 'passwordExpiry', expireDays > 0 ? expiry : 0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UserReset.clean = function (callback) {
|
|
|
|
|
async.waterfall([
|
|
|
|
|
function (next) {
|
|
|
|
|
async.parallel({
|
|
|
|
|
tokens: function (next) {
|
|
|
|
|
db.getSortedSetRangeByScore('reset:issueDate', 0, -1, '-inf', Date.now() - twoHours, next);
|
|
|
|
|
},
|
|
|
|
|
uids: function (next) {
|
|
|
|
|
db.getSortedSetRangeByScore('reset:issueDate:uid', 0, -1, '-inf', Date.now() - twoHours, next);
|
|
|
|
|
},
|
|
|
|
|
}, next);
|
|
|
|
|
},
|
|
|
|
|
function (results, next) {
|
|
|
|
|
if (!results.tokens.length && !results.uids.length) {
|
|
|
|
|
return next();
|
|
|
|
|
UserReset.clean = async function () {
|
|
|
|
|
const [tokens, uids] = await Promise.all([
|
|
|
|
|
db.getSortedSetRangeByScore('reset:issueDate', 0, -1, '-inf', Date.now() - twoHours),
|
|
|
|
|
db.getSortedSetRangeByScore('reset:issueDate:uid', 0, -1, '-inf', Date.now() - twoHours),
|
|
|
|
|
]);
|
|
|
|
|
if (!tokens.length && !uids.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
winston.verbose('[UserReset.clean] Removing ' + results.tokens.length + ' reset tokens from database');
|
|
|
|
|
async.parallel([
|
|
|
|
|
async.apply(db.deleteObjectFields, 'reset:uid', results.tokens),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'reset:issueDate', results.tokens),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'reset:issueDate:uid', results.uids),
|
|
|
|
|
], next);
|
|
|
|
|
},
|
|
|
|
|
], callback);
|
|
|
|
|
winston.verbose('[UserReset.clean] Removing ' + tokens.length + ' reset tokens from database');
|
|
|
|
|
await cleanTokensAndUids(tokens, uids);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UserReset.cleanByUid = function (uid, callback) {
|
|
|
|
|
var toClean = [];
|
|
|
|
|
UserReset.cleanByUid = async function (uid) {
|
|
|
|
|
const tokensToClean = [];
|
|
|
|
|
uid = parseInt(uid, 10);
|
|
|
|
|
|
|
|
|
|
async.waterfall([
|
|
|
|
|
function (next) {
|
|
|
|
|
batch.processSortedSet('reset:issueDate', function (tokens, next) {
|
|
|
|
|
db.getObjectFields('reset:uid', tokens, function (err, results) {
|
|
|
|
|
await batch.processSortedSet('reset:issueDate', async function (tokens) {
|
|
|
|
|
const results = await db.getObjectFields('reset:uid', tokens);
|
|
|
|
|
for (var code in results) {
|
|
|
|
|
if (results.hasOwnProperty(code) && parseInt(results[code], 10) === uid) {
|
|
|
|
|
toClean.push(code);
|
|
|
|
|
tokensToClean.push(code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, { batch: 500 });
|
|
|
|
|
|
|
|
|
|
next(err);
|
|
|
|
|
});
|
|
|
|
|
}, next);
|
|
|
|
|
},
|
|
|
|
|
function (next) {
|
|
|
|
|
if (!toClean.length) {
|
|
|
|
|
if (!tokensToClean.length) {
|
|
|
|
|
winston.verbose('[UserReset.cleanByUid] No tokens found for uid (' + uid + ').');
|
|
|
|
|
return setImmediate(next);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
winston.verbose('[UserReset.cleanByUid] Found ' + toClean.length + ' token(s), removing...');
|
|
|
|
|
async.parallel([
|
|
|
|
|
async.apply(db.deleteObjectFields, 'reset:uid', toClean),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'reset:issueDate', toClean),
|
|
|
|
|
async.apply(db.sortedSetRemove, 'reset:issueDate:uid', uid),
|
|
|
|
|
], next);
|
|
|
|
|
},
|
|
|
|
|
], callback);
|
|
|
|
|
winston.verbose('[UserReset.cleanByUid] Found ' + tokensToClean.length + ' token(s), removing...');
|
|
|
|
|
await cleanTokensAndUids(tokensToClean, uid);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function cleanTokensAndUids(tokens, uids) {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
db.deleteObjectFields('reset:uid', tokens),
|
|
|
|
|
db.sortedSetRemove('reset:issueDate', tokens),
|
|
|
|
|
db.sortedSetRemove('reset:issueDate:uid', uids),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|