2014-03-12 18:00:27 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
2020-10-21 16:30:14 -04:00
|
|
|
const nconf = require('nconf');
|
|
|
|
|
const winston = require('winston');
|
2014-03-12 18:00:27 -04:00
|
|
|
|
2020-10-21 16:30:14 -04:00
|
|
|
const user = require('./index');
|
2020-10-13 22:42:50 -04:00
|
|
|
const groups = require('../groups');
|
2020-10-21 16:30:14 -04:00
|
|
|
const utils = require('../utils');
|
|
|
|
|
const batch = require('../batch');
|
2014-03-12 18:00:27 -04:00
|
|
|
|
2020-10-21 16:30:14 -04:00
|
|
|
const db = require('../database');
|
|
|
|
|
const meta = require('../meta');
|
|
|
|
|
const emailer = require('../emailer');
|
2020-11-29 21:55:07 -05:00
|
|
|
const Password = require('../password');
|
2014-03-12 18:00:27 -04:00
|
|
|
|
2020-10-21 16:30:14 -04:00
|
|
|
const UserReset = module.exports;
|
2017-05-27 01:44:26 -04:00
|
|
|
|
2020-10-21 16:30:14 -04:00
|
|
|
const twoHours = 7200000;
|
2017-05-27 01:44:26 -04:00
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
UserReset.validate = async function (code) {
|
|
|
|
|
const uid = await db.getObjectField('reset:uid', code);
|
|
|
|
|
if (!uid) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const issueDate = await db.sortedSetScore('reset:issueDate', code);
|
|
|
|
|
return parseInt(issueDate, 10) > Date.now() - twoHours;
|
2017-05-27 01:44:26 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
UserReset.generate = async function (uid) {
|
|
|
|
|
const code = utils.generateUUID();
|
2021-06-11 14:38:56 -04:00
|
|
|
|
|
|
|
|
// Invalidate past tokens (must be done prior)
|
|
|
|
|
await UserReset.cleanByUid(uid);
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
await Promise.all([
|
|
|
|
|
db.setObjectField('reset:uid', code, uid),
|
|
|
|
|
db.sortedSetAdd('reset:issueDate', Date.now(), code),
|
|
|
|
|
]);
|
|
|
|
|
return code;
|
2017-05-27 01:44:26 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
async function canGenerate(uid) {
|
|
|
|
|
const score = await db.sortedSetScore('reset:issueDate:uid', uid);
|
|
|
|
|
if (score > Date.now() - (1000 * 60)) {
|
|
|
|
|
throw new Error('[[error:reset-rate-limited]]');
|
|
|
|
|
}
|
2017-05-27 01:44:26 -04:00
|
|
|
}
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
UserReset.send = async function (email) {
|
|
|
|
|
const uid = await user.getUidByEmail(email);
|
|
|
|
|
if (!uid) {
|
|
|
|
|
throw new Error('[[error:invalid-email]]');
|
|
|
|
|
}
|
|
|
|
|
await canGenerate(uid);
|
|
|
|
|
await db.sortedSetAdd('reset:issueDate:uid', Date.now(), uid);
|
|
|
|
|
const code = await UserReset.generate(uid);
|
|
|
|
|
await emailer.send('reset', uid, {
|
2021-02-03 23:59:08 -07:00
|
|
|
reset_link: `${nconf.get('url')}/reset/${code}`,
|
2019-07-16 20:44:00 -04:00
|
|
|
subject: '[[email:password-reset-requested]]',
|
|
|
|
|
template: 'reset',
|
|
|
|
|
uid: uid,
|
2022-01-28 15:25:33 -05:00
|
|
|
}).catch(err => winston.error(`[emailer.send] ${err.stack}`));
|
2021-06-11 15:43:03 -04:00
|
|
|
|
|
|
|
|
return code;
|
2017-05-27 01:44:26 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
UserReset.commit = async function (code, password) {
|
2019-09-11 00:28:42 -04:00
|
|
|
user.isPasswordValid(password);
|
2019-07-16 20:44:00 -04:00
|
|
|
const validated = await UserReset.validate(code);
|
|
|
|
|
if (!validated) {
|
|
|
|
|
throw new Error('[[error:reset-code-not-valid]]');
|
|
|
|
|
}
|
|
|
|
|
const uid = await db.getObjectField('reset:uid', code);
|
|
|
|
|
if (!uid) {
|
|
|
|
|
throw new Error('[[error:reset-code-not-valid]]');
|
|
|
|
|
}
|
2020-11-29 21:55:07 -05:00
|
|
|
const userData = await db.getObjectFields(
|
2021-02-03 23:59:08 -07:00
|
|
|
`user:${uid}`,
|
2020-11-29 21:55:07 -05:00
|
|
|
['password', 'passwordExpiry', 'password:shaWrapped']
|
|
|
|
|
);
|
|
|
|
|
const ok = await Password.compare(password, userData.password, !!parseInt(userData['password:shaWrapped'], 10));
|
|
|
|
|
if (ok) {
|
|
|
|
|
throw new Error('[[error:reset-same-password]]');
|
|
|
|
|
}
|
2019-07-16 20:44:00 -04:00
|
|
|
const hash = await user.hashPassword(password);
|
2020-11-29 21:55:07 -05:00
|
|
|
const data = {
|
|
|
|
|
password: hash,
|
|
|
|
|
'password:shaWrapped': 1,
|
|
|
|
|
};
|
2019-07-16 20:44:00 -04:00
|
|
|
|
2020-11-29 21:55:07 -05:00
|
|
|
// don't verify email if password reset is due to expiry
|
|
|
|
|
const isPasswordExpired = userData.passwordExpiry && userData.passwordExpiry < Date.now();
|
|
|
|
|
if (!isPasswordExpired) {
|
2021-11-18 16:42:18 -05:00
|
|
|
data['email:confirmed'] = 1;
|
2020-11-29 21:55:07 -05:00
|
|
|
await groups.join('verified-users', uid);
|
|
|
|
|
await groups.leave('unverified-users', uid);
|
|
|
|
|
}
|
2022-01-12 11:08:34 -05:00
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
user.setUserFields(uid, data),
|
|
|
|
|
db.deleteObjectField('reset:uid', code),
|
|
|
|
|
db.sortedSetRemoveBulk([
|
|
|
|
|
['reset:issueDate', code],
|
|
|
|
|
['reset:issueDate:uid', uid],
|
|
|
|
|
]),
|
|
|
|
|
user.reset.updateExpiry(uid),
|
|
|
|
|
user.auth.resetLockout(uid),
|
2022-01-12 11:09:02 -05:00
|
|
|
user.auth.revokeAllSessions(uid),
|
2022-01-12 11:08:34 -05:00
|
|
|
user.email.expireValidation(uid),
|
2019-07-16 20:44:00 -04:00
|
|
|
]);
|
2017-05-27 01:44:26 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
UserReset.updateExpiry = async function (uid) {
|
|
|
|
|
const expireDays = meta.config.passwordExpiryDays;
|
2019-10-07 23:13:43 -04:00
|
|
|
if (expireDays > 0) {
|
2020-10-21 16:30:14 -04:00
|
|
|
const oneDay = 1000 * 60 * 60 * 24;
|
|
|
|
|
const expiry = Date.now() + (oneDay * expireDays);
|
2019-10-07 23:13:43 -04:00
|
|
|
await user.setUserField(uid, 'passwordExpiry', expiry);
|
2020-10-21 16:30:14 -04:00
|
|
|
} else {
|
2021-02-03 23:59:08 -07:00
|
|
|
await db.deleteObjectField(`user:${uid}`, 'passwordExpiry');
|
2019-10-07 23:13:43 -04:00
|
|
|
}
|
2017-05-27 01:44:26 -04:00
|
|
|
};
|
|
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-03 23:59:08 -07:00
|
|
|
winston.verbose(`[UserReset.clean] Removing ${tokens.length} reset tokens from database`);
|
2019-07-16 20:44:00 -04:00
|
|
|
await cleanTokensAndUids(tokens, uids);
|
2017-05-27 01:44:26 -04:00
|
|
|
};
|
2017-08-16 14:36:51 -04:00
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
UserReset.cleanByUid = async function (uid) {
|
|
|
|
|
const tokensToClean = [];
|
2017-08-16 14:36:51 -04:00
|
|
|
uid = parseInt(uid, 10);
|
|
|
|
|
|
2021-02-04 00:01:39 -07:00
|
|
|
await batch.processSortedSet('reset:issueDate', async (tokens) => {
|
2019-07-16 20:44:00 -04:00
|
|
|
const results = await db.getObjectFields('reset:uid', tokens);
|
2021-02-04 01:34:30 -07:00
|
|
|
for (const [code, result] of Object.entries(results)) {
|
|
|
|
|
if (parseInt(result, 10) === uid) {
|
2019-07-16 20:44:00 -04:00
|
|
|
tokensToClean.push(code);
|
2017-08-16 14:36:51 -04:00
|
|
|
}
|
2019-07-16 20:44:00 -04:00
|
|
|
}
|
|
|
|
|
}, { batch: 500 });
|
2017-08-16 14:36:51 -04:00
|
|
|
|
2019-07-16 20:44:00 -04:00
|
|
|
if (!tokensToClean.length) {
|
2021-02-03 23:59:08 -07:00
|
|
|
winston.verbose(`[UserReset.cleanByUid] No tokens found for uid (${uid}).`);
|
2019-07-16 20:44:00 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-03 23:59:08 -07:00
|
|
|
winston.verbose(`[UserReset.cleanByUid] Found ${tokensToClean.length} token(s), removing...`);
|
2019-07-16 20:44:00 -04:00
|
|
|
await cleanTokensAndUids(tokensToClean, uid);
|
2017-08-16 14:36:51 -04:00
|
|
|
};
|
2019-07-16 20:44:00 -04:00
|
|
|
|
|
|
|
|
async function cleanTokensAndUids(tokens, uids) {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
db.deleteObjectFields('reset:uid', tokens),
|
|
|
|
|
db.sortedSetRemove('reset:issueDate', tokens),
|
|
|
|
|
db.sortedSetRemove('reset:issueDate:uid', uids),
|
|
|
|
|
]);
|
|
|
|
|
}
|