diff --git a/src/emailer.js b/src/emailer.js index 510ea4f19e..996dab43ab 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -37,7 +37,7 @@ var app; var viewsDir = nconf.get('views_dir'); -Emailer.getTemplates = function (config, cb) { +Emailer.getTemplates = function (config, callback) { var emailsPath = path.join(viewsDir, 'emails'); async.waterfall([ function (next) { @@ -71,7 +71,7 @@ Emailer.getTemplates = function (config, cb) { ], next); }, next); }, - ], cb); + ], callback); }; Emailer.listServices = function (callback) { @@ -407,3 +407,5 @@ function getHostname() { return parsed.hostname; } + +require('./promisify')(Emailer, ['transports']); diff --git a/src/notifications.js b/src/notifications.js index 19fb157d2a..731020c0ba 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -320,6 +320,9 @@ function pushToUids(uids, notification, callback) { Notifications.pushGroup = function (notification, groupName, callback) { callback = callback || function () {}; + if (!notification) { + return callback(); + } async.waterfall([ function (next) { groups.getMembers(groupName, 0, -1, next); @@ -332,6 +335,9 @@ Notifications.pushGroup = function (notification, groupName, callback) { Notifications.pushGroups = function (notification, groupNames, callback) { callback = callback || function () {}; + if (!notification) { + return callback(); + } async.waterfall([ function (next) { groups.getMembersOfGroups(groupNames, next); diff --git a/src/user/approval.js b/src/user/approval.js index 32df3fcf85..72d2953da8 100644 --- a/src/user/approval.js +++ b/src/user/approval.js @@ -12,228 +12,132 @@ var utils = require('../utils'); var plugins = require('../plugins'); module.exports = function (User) { - User.addToApprovalQueue = function (userData, callback) { + User.addToApprovalQueue = async function (userData) { userData.userslug = utils.slugify(userData.username); - async.waterfall([ - function (next) { - canQueue(userData, next); - }, - function (next) { - User.hashPassword(userData.password, next); - }, - function (hashedPassword, next) { - var data = { - username: userData.username, - email: userData.email, - ip: userData.ip, - hashedPassword: hashedPassword, - }; - plugins.fireHook('filter:user.addToApprovalQueue', { data: data, userData: userData }, next); - }, - function (results, next) { - db.setObject('registration:queue:name:' + userData.username, results.data, next); - }, - function (next) { - db.sortedSetAdd('registration:queue', Date.now(), userData.username, next); - }, - function (next) { - sendNotificationToAdmins(userData.username, next); - }, - ], callback); + await canQueue(userData); + const hashedPassword = await User.hashPassword(userData.password); + var data = { + username: userData.username, + email: userData.email, + ip: userData.ip, + hashedPassword: hashedPassword, + }; + const results = await plugins.fireHook('filter:user.addToApprovalQueue', { data: data, userData: userData }); + await db.setObject('registration:queue:name:' + userData.username, results.data); + await db.sortedSetAdd('registration:queue', Date.now(), userData.username); + await sendNotificationToAdmins(userData.username); }; - function canQueue(userData, callback) { - async.waterfall([ - function (next) { - User.isDataValid(userData, next); - }, - function (next) { - db.getSortedSetRange('registration:queue', 0, -1, next); - }, - function (usernames, next) { - if (usernames.includes(userData.username)) { - return next(new Error('[[error:username-taken]]')); - } - const keys = usernames.filter(Boolean).map(username => 'registration:queue:name:' + username); - db.getObjectsFields(keys, ['email'], next); - }, - function (data, next) { - const emails = data.map(data => data && data.email); - if (emails.includes(userData.email)) { - return next(new Error('[[error:email-taken]]')); - } - next(); - }, - ], callback); + async function canQueue(userData) { + await User.isDataValid(userData); + const usernames = await db.getSortedSetRange('registration:queue', 0, -1); + if (usernames.includes(userData.username)) { + throw new Error('[[error:username-taken]]'); + } + const keys = usernames.filter(Boolean).map(username => 'registration:queue:name:' + username); + const data = await db.getObjectsFields(keys, ['email']); + const emails = data.map(data => data && data.email); + if (emails.includes(userData.email)) { + throw new Error('[[error:email-taken]]'); + } } - function sendNotificationToAdmins(username, callback) { - async.waterfall([ - function (next) { - notifications.create({ - type: 'new-register', - bodyShort: '[[notifications:new_register, ' + username + ']]', - nid: 'new_register:' + username, - path: '/admin/manage/registration', - mergeId: 'new_register', - }, next); - }, - function (notification, next) { - notifications.pushGroup(notification, 'administrators', next); - }, - ], callback); + async function sendNotificationToAdmins(username) { + const notifObj = await notifications.create({ + type: 'new-register', + bodyShort: '[[notifications:new_register, ' + username + ']]', + nid: 'new_register:' + username, + path: '/admin/manage/registration', + mergeId: 'new_register', + }); + await notifications.pushGroup(notifObj, 'administrators'); } - User.acceptRegistration = function (username, callback) { - var uid; - var userData; - async.waterfall([ - function (next) { - db.getObject('registration:queue:name:' + username, next); - }, - function (_userData, next) { - if (!_userData) { - return callback(new Error('[[error:invalid-data]]')); - } - userData = _userData; - User.create(userData, next); - }, - function (_uid, next) { - uid = _uid; - User.setUserField(uid, 'password', userData.hashedPassword, next); - }, - function (next) { - removeFromQueue(username, next); - }, - function (next) { - markNotificationRead(username, next); - }, - function (next) { - plugins.fireHook('filter:register.complete', { uid: uid }, next); - }, - function (result, next) { - var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; - var data = { - username: username, - subject: '[[email:welcome-to, ' + title + ']]', - template: 'registration_accepted', - uid: uid, - }; + User.acceptRegistration = async function (username) { + const userData = await db.getObject('registration:queue:name:' + username); + if (!userData) { + throw new Error('[[error:invalid-data]]'); + } - emailer.send('registration_accepted', uid, data, next); - }, - function (next) { - next(null, uid); - }, - ], callback); + const uid = await User.create(userData); + await User.setUserField(uid, 'password', userData.hashedPassword); + await removeFromQueue(username); + await markNotificationRead(username); + await plugins.fireHook('filter:register.complete', { uid: uid }); + await emailer.send('registration_accepted', uid, { + username: username, + subject: '[[email:welcome-to, ' + (meta.config.title || meta.config.browserTitle || 'NodeBB') + ']]', + template: 'registration_accepted', + uid: uid, + }); + return uid; }; - function markNotificationRead(username, callback) { - var nid = 'new_register:' + username; - async.waterfall([ - function (next) { - groups.getMembers('administrators', 0, -1, next); - }, - function (uids, next) { - async.each(uids, function (uid, next) { - notifications.markRead(nid, uid, next); - }, next); - }, - ], callback); + async function markNotificationRead(username) { + const nid = 'new_register:' + username; + const uids = await groups.getMembers('administrators', 0, -1); + const promises = uids.map(uid => notifications.markRead(nid, uid)); + await Promise.all(promises); } - User.rejectRegistration = function (username, callback) { - async.waterfall([ - function (next) { - removeFromQueue(username, next); - }, - function (next) { - markNotificationRead(username, next); - }, - ], callback); + User.rejectRegistration = async function (username) { + await removeFromQueue(username); + await markNotificationRead(username); }; - function removeFromQueue(username, callback) { - async.parallel([ - async.apply(db.sortedSetRemove, 'registration:queue', username), - async.apply(db.delete, 'registration:queue:name:' + username), - ], function (err) { - callback(err); - }); + async function removeFromQueue(username) { + await Promise.all([ + db.sortedSetRemove('registration:queue', username), + db.delete('registration:queue:name:' + username), + ]); } - User.shouldQueueUser = function (ip, callback) { + User.shouldQueueUser = async function (ip) { const registrationApprovalType = meta.config.registrationApprovalType; if (registrationApprovalType === 'admin-approval') { - setImmediate(callback, null, true); + return true; } else if (registrationApprovalType === 'admin-approval-ip') { - db.sortedSetCard('ip:' + ip + ':uid', function (err, count) { - callback(err, !!count); - }); - } else { - setImmediate(callback, null, false); + const count = await db.sortedSetCard('ip:' + ip + ':uid'); + return !!count; } + return false; }; - User.getRegistrationQueue = function (start, stop, callback) { - var data; - async.waterfall([ - function (next) { - db.getSortedSetRevRangeWithScores('registration:queue', start, stop, next); - }, - function (_data, next) { - data = _data; - var keys = data.filter(Boolean).map(user => 'registration:queue:name:' + user.value); - db.getObjects(keys, next); - }, - function (users, next) { - users = users.filter(Boolean).map(function (user, index) { - user.timestampISO = utils.toISOString(data[index].score); - user.email = validator.escape(String(user.email)); - delete user.hashedPassword; - return user; - }); + User.getRegistrationQueue = async function (start, stop) { + const data = await db.getSortedSetRevRangeWithScores('registration:queue', start, stop); + const keys = data.filter(Boolean).map(user => 'registration:queue:name:' + user.value); + let users = await db.getObjects(keys); + users = users.filter(Boolean).map(function (user, index) { + user.timestampISO = utils.toISOString(data[index].score); + user.email = validator.escape(String(user.email)); + delete user.hashedPassword; + return user; + }); - async.map(users, function (user, next) { - // temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392 - // need to keep this for getIPMatchedUsers - user.ip = user.ip.replace('::ffff:', ''); - getIPMatchedUsers(user, function (err) { - next(err, user); - }); - user.customActions = [].concat(user.customActions); - /* - // then spam prevention plugins, using the "filter:user.getRegistrationQueue" hook can be like: - user.customActions.push({ - title: '[[spam-be-gone:report-user]]', - id: 'report-spam-user-' + user.username, - class: 'btn-warning report-spam-user', - icon: 'fa-flag' - }); - */ - }, next); - }, - function (users, next) { - plugins.fireHook('filter:user.getRegistrationQueue', { users: users }, next); - }, - function (results, next) { - next(null, results.users); - }, - ], callback); + users = await async.map(users, async function (user) { + // temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392 + // need to keep this for getIPMatchedUsers + user.ip = user.ip.replace('::ffff:', ''); + await getIPMatchedUsers(user); + user.customActions = [].concat(user.customActions); + return user; + /* + // then spam prevention plugins, using the "filter:user.getRegistrationQueue" hook can be like: + user.customActions.push({ + title: '[[spam-be-gone:report-user]]', + id: 'report-spam-user-' + user.username, + class: 'btn-warning report-spam-user', + icon: 'fa-flag' + }); + */ + }); + const results = await plugins.fireHook('filter:user.getRegistrationQueue', { users: users }); + return results.users; }; - function getIPMatchedUsers(user, callback) { - async.waterfall([ - function (next) { - User.getUidsFromSet('ip:' + user.ip + ':uid', 0, -1, next); - }, - function (uids, next) { - User.getUsersFields(uids, ['uid', 'username', 'picture'], next); - }, - function (data, next) { - user.ipMatch = data; - next(); - }, - ], callback); + async function getIPMatchedUsers(user) { + const uids = await User.getUidsFromSet('ip:' + user.ip + ':uid', 0, -1); + const data = User.getUsersFields(uids, ['uid', 'username', 'picture']); + user.ipMatch = data; } }; diff --git a/src/user/auth.js b/src/user/auth.js index 1d2e2ddc27..d501cac0e2 100644 --- a/src/user/auth.js +++ b/src/user/auth.js @@ -3,6 +3,8 @@ var async = require('async'); var winston = require('winston'); var validator = require('validator'); +const util = require('util'); +const _ = require('lodash'); var db = require('../database'); var meta = require('../meta'); var events = require('../events'); @@ -12,207 +14,135 @@ var utils = require('../utils'); module.exports = function (User) { User.auth = {}; - User.auth.logAttempt = function (uid, ip, callback) { + User.auth.logAttempt = async function (uid, ip) { if (!(parseInt(uid, 10) > 0)) { - return setImmediate(callback); + return; } - async.waterfall([ - function (next) { - db.exists('lockout:' + uid, next); - }, - function (exists, next) { - if (exists) { - return callback(new Error('[[error:account-locked]]')); - } - db.increment('loginAttempts:' + uid, next); - }, - function (attempts, next) { - if (attempts <= meta.config.loginAttempts) { - return db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60, callback); - } - // Lock out the account - db.set('lockout:' + uid, '', next); - }, - function (next) { - var duration = 1000 * 60 * meta.config.lockoutDuration; - - db.delete('loginAttempts:' + uid); - db.pexpire('lockout:' + uid, duration); - events.log({ - type: 'account-locked', - uid: uid, - ip: ip, - }); - next(new Error('[[error:account-locked]]')); - }, - ], callback); + const exists = await db.exists('lockout:' + uid); + if (exists) { + throw new Error('[[error:account-locked]]'); + } + const attempts = await db.increment('loginAttempts:' + uid); + if (attempts <= meta.config.loginAttempts) { + return await db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60); + } + // Lock out the account + await db.set('lockout:' + uid, ''); + var duration = 1000 * 60 * meta.config.lockoutDuration; + + await db.delete('loginAttempts:' + uid); + await db.pexpire('lockout:' + uid, duration); + events.log({ + type: 'account-locked', + uid: uid, + ip: ip, + }); + throw new Error('[[error:account-locked]]'); }; - User.auth.getFeedToken = function (uid, callback) { - if (parseInt(uid, 10) <= 0) { - return setImmediate(callback); + User.auth.getFeedToken = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + var _token = await db.getObjectField('user:' + uid, 'rss_token'); + const token = _token || utils.generateUUID(); + if (!_token) { + await User.setUserField(uid, 'rss_token', token); } - var token; - async.waterfall([ - function (next) { - db.getObjectField('user:' + uid, 'rss_token', next); - }, - function (_token, next) { - token = _token || utils.generateUUID(); - if (!_token) { - User.setUserField(uid, 'rss_token', token, next); - } else { - next(); - } - }, - function (next) { - next(null, token); - }, - ], callback); + return token; }; - User.auth.clearLoginAttempts = function (uid) { - db.delete('loginAttempts:' + uid); + User.auth.clearLoginAttempts = async function (uid) { + await db.delete('loginAttempts:' + uid); }; - User.auth.resetLockout = function (uid, callback) { - async.parallel([ - async.apply(db.delete, 'loginAttempts:' + uid), - async.apply(db.delete, 'lockout:' + uid), - ], callback); + User.auth.resetLockout = async function (uid) { + await db.deleteAll([ + 'loginAttempts:' + uid, + 'lockout:' + uid, + ]); }; - User.auth.getSessions = function (uid, curSessionId, callback) { - var _sids; - - // curSessionId is optional - if (arguments.length === 2 && typeof curSessionId === 'function') { - callback = curSessionId; - curSessionId = undefined; - } - - async.waterfall([ - async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':sessions', 0, 19), - function (sids, next) { - _sids = sids; - async.map(sids, db.sessionStore.get.bind(db.sessionStore), next); - }, - function (sessions, next) { - sessions.forEach(function (sessionObj, idx) { - if (sessionObj && sessionObj.meta) { - sessionObj.meta.current = curSessionId === _sids[idx]; - } - }); - - // Revoke any sessions that have expired, return filtered list - var expiredSids = []; - var expired; - - sessions = sessions.filter(function (sessionObj, idx) { - expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || - !sessionObj.passport.hasOwnProperty('user') || - parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); - - if (expired) { - expiredSids.push(_sids[idx]); - } - - return !expired; - }); - - async.each(expiredSids, function (sid, next) { - User.auth.revokeSession(sid, uid, next); - }, function (err) { - next(err, sessions); - }); - }, - function (sessions, next) { - sessions = sessions.map(function (sessObj) { - if (sessObj.meta) { - sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); - sessObj.meta.ip = validator.escape(String(sessObj.meta.ip)); - } - return sessObj.meta; - }).filter(Boolean); - next(null, sessions); - }, - ], callback); + User.auth.getSessions = async function (uid, curSessionId) { + const sids = await db.getSortedSetRevRange('uid:' + uid + ':sessions', 0, 19); + let sessions = await async.map(sids, db.sessionStore.get.bind(db.sessionStore)); + sessions.forEach(function (sessionObj, idx) { + if (sessionObj && sessionObj.meta) { + sessionObj.meta.current = curSessionId === sids[idx]; + } + }); + + // Revoke any sessions that have expired, return filtered list + var expiredSids = []; + var expired; + + sessions = sessions.filter(function (sessionObj, idx) { + expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || + !sessionObj.passport.hasOwnProperty('user') || + parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); + + if (expired) { + expiredSids.push(sids[idx]); + } + + return !expired; + }); + await Promise.all(expiredSids.map(s => User.auth.revokeSession(s, uid))); + + sessions = sessions.map(function (sessObj) { + if (sessObj.meta) { + sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); + sessObj.meta.ip = validator.escape(String(sessObj.meta.ip)); + } + return sessObj.meta; + }).filter(Boolean); + return sessions; }; - User.auth.addSession = function (uid, sessionId, callback) { - callback = callback || function () {}; + User.auth.addSession = async function (uid, sessionId) { if (!(parseInt(uid, 10) > 0)) { - return setImmediate(callback); + return; } - db.sortedSetAdd('uid:' + uid + ':sessions', Date.now(), sessionId, callback); + await db.sortedSetAdd('uid:' + uid + ':sessions', Date.now(), sessionId); }; - User.auth.revokeSession = function (sessionId, uid, callback) { - winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid); + const getSessionFromStore = util.promisify(function (sessionId, callback) { + db.sessionStore.get(sessionId, function (err, sessionObj) { + callback(err, sessionObj || null); + }); + }); - async.waterfall([ - function (next) { - db.sessionStore.get(sessionId, function (err, sessionObj) { - next(err, sessionObj || null); - }); - }, - function (sessionObj, next) { - async.parallel([ - function (next) { - if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) { - db.deleteObjectField('uid:' + uid + ':sessionUUID:sessionId', sessionObj.meta.uuid, next); - } else { - next(); - } - }, - async.apply(db.sortedSetRemove, 'uid:' + uid + ':sessions', sessionId), - async.apply(db.sessionStore.destroy.bind(db.sessionStore), sessionId), - ], function (err) { - next(err); - }); - }, - ], callback); + User.auth.revokeSession = async function (sessionId, uid) { + winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid); + const sessionObj = await getSessionFromStore(sessionId); + if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) { + await db.deleteObjectField('uid:' + uid + ':sessionUUID:sessionId', sessionObj.meta.uuid); + } + await async.parallel([ + async.apply(db.sortedSetRemove, 'uid:' + uid + ':sessions', sessionId), + async.apply(db.sessionStore.destroy.bind(db.sessionStore), sessionId), + ]); }; - User.auth.revokeAllSessions = function (uid, callback) { - async.waterfall([ - async.apply(db.getSortedSetRange, 'uid:' + uid + ':sessions', 0, -1), - function (sids, next) { - async.each(sids, function (sid, next) { - User.auth.revokeSession(sid, uid, next); - }, next); - }, - ], callback); + User.auth.revokeAllSessions = async function (uid) { + const sids = await db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1); + const promises = sids.map(s => User.auth.revokeSession(s, uid)); + await Promise.all(promises); }; - User.auth.deleteAllSessions = function (callback) { - var _ = require('lodash'); - batch.processSortedSet('users:joindate', function (uids, next) { - var sessionKeys = uids.map(function (uid) { - return 'uid:' + uid + ':sessions'; - }); - - var sessionUUIDKeys = uids.map(function (uid) { - return 'uid:' + uid + ':sessionUUID:sessionId'; - }); - - async.waterfall([ + User.auth.deleteAllSessions = async function () { + await batch.processSortedSet('users:joindate', async function (uids) { + const sessionKeys = uids.map(uid => 'uid:' + uid + ':sessions'); + const sessionUUIDKeys = uids.map(uid => 'uid:' + uid + ':sessionUUID:sessionId'); + const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1)); + await async.parallel([ + async.apply(db.deleteAll, sessionKeys.concat(sessionUUIDKeys)), function (next) { - db.getSortedSetRange(sessionKeys, 0, -1, next); - }, - function (sids, next) { - sids = _.flatten(sids); - async.parallel([ - async.apply(db.deleteAll, sessionUUIDKeys), - async.apply(db.deleteAll, sessionKeys), - function (next) { - async.each(sids, function (sid, next) { - db.sessionStore.destroy(sid, next); - }, next); - }, - ], next); + async.each(sids, function (sid, next) { + db.sessionStore.destroy(sid, next); + }, next); }, - ], next); - }, { batch: 1000 }, callback); + ]); + }, { batch: 1000 }); }; };