feat: #7743, user/approval, user/auth

v1.18.x
Barış Soner Uşaklı 6 years ago
parent 2b70e86f21
commit b24ce97602

@ -37,7 +37,7 @@ var app;
var viewsDir = nconf.get('views_dir'); var viewsDir = nconf.get('views_dir');
Emailer.getTemplates = function (config, cb) { Emailer.getTemplates = function (config, callback) {
var emailsPath = path.join(viewsDir, 'emails'); var emailsPath = path.join(viewsDir, 'emails');
async.waterfall([ async.waterfall([
function (next) { function (next) {
@ -71,7 +71,7 @@ Emailer.getTemplates = function (config, cb) {
], next); ], next);
}, next); }, next);
}, },
], cb); ], callback);
}; };
Emailer.listServices = function (callback) { Emailer.listServices = function (callback) {
@ -407,3 +407,5 @@ function getHostname() {
return parsed.hostname; return parsed.hostname;
} }
require('./promisify')(Emailer, ['transports']);

@ -320,6 +320,9 @@ function pushToUids(uids, notification, callback) {
Notifications.pushGroup = function (notification, groupName, callback) { Notifications.pushGroup = function (notification, groupName, callback) {
callback = callback || function () {}; callback = callback || function () {};
if (!notification) {
return callback();
}
async.waterfall([ async.waterfall([
function (next) { function (next) {
groups.getMembers(groupName, 0, -1, next); groups.getMembers(groupName, 0, -1, next);
@ -332,6 +335,9 @@ Notifications.pushGroup = function (notification, groupName, callback) {
Notifications.pushGroups = function (notification, groupNames, callback) { Notifications.pushGroups = function (notification, groupNames, callback) {
callback = callback || function () {}; callback = callback || function () {};
if (!notification) {
return callback();
}
async.waterfall([ async.waterfall([
function (next) { function (next) {
groups.getMembersOfGroups(groupNames, next); groups.getMembersOfGroups(groupNames, next);

@ -12,181 +12,101 @@ var utils = require('../utils');
var plugins = require('../plugins'); var plugins = require('../plugins');
module.exports = function (User) { module.exports = function (User) {
User.addToApprovalQueue = function (userData, callback) { User.addToApprovalQueue = async function (userData) {
userData.userslug = utils.slugify(userData.username); userData.userslug = utils.slugify(userData.username);
async.waterfall([ await canQueue(userData);
function (next) { const hashedPassword = await User.hashPassword(userData.password);
canQueue(userData, next);
},
function (next) {
User.hashPassword(userData.password, next);
},
function (hashedPassword, next) {
var data = { var data = {
username: userData.username, username: userData.username,
email: userData.email, email: userData.email,
ip: userData.ip, ip: userData.ip,
hashedPassword: hashedPassword, hashedPassword: hashedPassword,
}; };
plugins.fireHook('filter:user.addToApprovalQueue', { data: data, userData: userData }, next); const results = await plugins.fireHook('filter:user.addToApprovalQueue', { data: data, userData: userData });
}, await db.setObject('registration:queue:name:' + userData.username, results.data);
function (results, next) { await db.sortedSetAdd('registration:queue', Date.now(), userData.username);
db.setObject('registration:queue:name:' + userData.username, results.data, next); await sendNotificationToAdmins(userData.username);
},
function (next) {
db.sortedSetAdd('registration:queue', Date.now(), userData.username, next);
},
function (next) {
sendNotificationToAdmins(userData.username, next);
},
], callback);
}; };
function canQueue(userData, callback) { async function canQueue(userData) {
async.waterfall([ await User.isDataValid(userData);
function (next) { const usernames = await db.getSortedSetRange('registration:queue', 0, -1);
User.isDataValid(userData, next);
},
function (next) {
db.getSortedSetRange('registration:queue', 0, -1, next);
},
function (usernames, next) {
if (usernames.includes(userData.username)) { if (usernames.includes(userData.username)) {
return next(new Error('[[error:username-taken]]')); throw new Error('[[error:username-taken]]');
} }
const keys = usernames.filter(Boolean).map(username => 'registration:queue:name:' + username); const keys = usernames.filter(Boolean).map(username => 'registration:queue:name:' + username);
db.getObjectsFields(keys, ['email'], next); const data = await db.getObjectsFields(keys, ['email']);
},
function (data, next) {
const emails = data.map(data => data && data.email); const emails = data.map(data => data && data.email);
if (emails.includes(userData.email)) { if (emails.includes(userData.email)) {
return next(new Error('[[error:email-taken]]')); throw new Error('[[error:email-taken]]');
} }
next();
},
], callback);
} }
function sendNotificationToAdmins(username, callback) { async function sendNotificationToAdmins(username) {
async.waterfall([ const notifObj = await notifications.create({
function (next) {
notifications.create({
type: 'new-register', type: 'new-register',
bodyShort: '[[notifications:new_register, ' + username + ']]', bodyShort: '[[notifications:new_register, ' + username + ']]',
nid: 'new_register:' + username, nid: 'new_register:' + username,
path: '/admin/manage/registration', path: '/admin/manage/registration',
mergeId: 'new_register', mergeId: 'new_register',
}, next); });
}, await notifications.pushGroup(notifObj, 'administrators');
function (notification, next) {
notifications.pushGroup(notification, 'administrators', next);
},
], callback);
} }
User.acceptRegistration = function (username, callback) { User.acceptRegistration = async function (username) {
var uid; const userData = await db.getObject('registration:queue:name:' + username);
var userData; if (!userData) {
async.waterfall([ throw new Error('[[error:invalid-data]]');
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); const uid = await User.create(userData);
}, await User.setUserField(uid, 'password', userData.hashedPassword);
function (_uid, next) { await removeFromQueue(username);
uid = _uid; await markNotificationRead(username);
User.setUserField(uid, 'password', userData.hashedPassword, next); await plugins.fireHook('filter:register.complete', { uid: uid });
}, await emailer.send('registration_accepted', uid, {
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, username: username,
subject: '[[email:welcome-to, ' + title + ']]', subject: '[[email:welcome-to, ' + (meta.config.title || meta.config.browserTitle || 'NodeBB') + ']]',
template: 'registration_accepted', template: 'registration_accepted',
uid: uid, uid: uid,
});
return uid;
}; };
emailer.send('registration_accepted', uid, data, next); async function markNotificationRead(username) {
}, const nid = 'new_register:' + username;
function (next) { const uids = await groups.getMembers('administrators', 0, -1);
next(null, uid); const promises = uids.map(uid => notifications.markRead(nid, uid));
}, await Promise.all(promises);
], callback);
};
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);
} }
User.rejectRegistration = function (username, callback) { User.rejectRegistration = async function (username) {
async.waterfall([ await removeFromQueue(username);
function (next) { await markNotificationRead(username);
removeFromQueue(username, next);
},
function (next) {
markNotificationRead(username, next);
},
], callback);
}; };
function removeFromQueue(username, callback) { async function removeFromQueue(username) {
async.parallel([ await Promise.all([
async.apply(db.sortedSetRemove, 'registration:queue', username), db.sortedSetRemove('registration:queue', username),
async.apply(db.delete, 'registration:queue:name:' + username), db.delete('registration:queue:name:' + username),
], function (err) { ]);
callback(err);
});
} }
User.shouldQueueUser = function (ip, callback) { User.shouldQueueUser = async function (ip) {
const registrationApprovalType = meta.config.registrationApprovalType; const registrationApprovalType = meta.config.registrationApprovalType;
if (registrationApprovalType === 'admin-approval') { if (registrationApprovalType === 'admin-approval') {
setImmediate(callback, null, true); return true;
} else if (registrationApprovalType === 'admin-approval-ip') { } else if (registrationApprovalType === 'admin-approval-ip') {
db.sortedSetCard('ip:' + ip + ':uid', function (err, count) { const count = await db.sortedSetCard('ip:' + ip + ':uid');
callback(err, !!count); return !!count;
});
} else {
setImmediate(callback, null, false);
} }
return false;
}; };
User.getRegistrationQueue = function (start, stop, callback) { User.getRegistrationQueue = async function (start, stop) {
var data; const data = await db.getSortedSetRevRangeWithScores('registration:queue', start, stop);
async.waterfall([ const keys = data.filter(Boolean).map(user => 'registration:queue:name:' + user.value);
function (next) { let users = await db.getObjects(keys);
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) { users = users.filter(Boolean).map(function (user, index) {
user.timestampISO = utils.toISOString(data[index].score); user.timestampISO = utils.toISOString(data[index].score);
user.email = validator.escape(String(user.email)); user.email = validator.escape(String(user.email));
@ -194,14 +114,13 @@ module.exports = function (User) {
return user; return user;
}); });
async.map(users, function (user, next) { users = await async.map(users, async function (user) {
// temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392 // temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392
// need to keep this for getIPMatchedUsers // need to keep this for getIPMatchedUsers
user.ip = user.ip.replace('::ffff:', ''); user.ip = user.ip.replace('::ffff:', '');
getIPMatchedUsers(user, function (err) { await getIPMatchedUsers(user);
next(err, user);
});
user.customActions = [].concat(user.customActions); user.customActions = [].concat(user.customActions);
return user;
/* /*
// then spam prevention plugins, using the "filter:user.getRegistrationQueue" hook can be like: // then spam prevention plugins, using the "filter:user.getRegistrationQueue" hook can be like:
user.customActions.push({ user.customActions.push({
@ -211,29 +130,14 @@ module.exports = function (User) {
icon: 'fa-flag' icon: 'fa-flag'
}); });
*/ */
}, next); });
}, const results = await plugins.fireHook('filter:user.getRegistrationQueue', { users: users });
function (users, next) { return results.users;
plugins.fireHook('filter:user.getRegistrationQueue', { users: users }, next);
},
function (results, next) {
next(null, results.users);
},
], callback);
}; };
function getIPMatchedUsers(user, callback) { async function getIPMatchedUsers(user) {
async.waterfall([ const uids = await User.getUidsFromSet('ip:' + user.ip + ':uid', 0, -1);
function (next) { const data = User.getUsersFields(uids, ['uid', 'username', 'picture']);
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; user.ipMatch = data;
next();
},
], callback);
} }
}; };

@ -3,6 +3,8 @@
var async = require('async'); var async = require('async');
var winston = require('winston'); var winston = require('winston');
var validator = require('validator'); var validator = require('validator');
const util = require('util');
const _ = require('lodash');
var db = require('../database'); var db = require('../database');
var meta = require('../meta'); var meta = require('../meta');
var events = require('../events'); var events = require('../events');
@ -12,95 +14,61 @@ var utils = require('../utils');
module.exports = function (User) { module.exports = function (User) {
User.auth = {}; User.auth = {};
User.auth.logAttempt = function (uid, ip, callback) { User.auth.logAttempt = async function (uid, ip) {
if (!(parseInt(uid, 10) > 0)) { if (!(parseInt(uid, 10) > 0)) {
return setImmediate(callback); return;
} }
async.waterfall([ const exists = await db.exists('lockout:' + uid);
function (next) {
db.exists('lockout:' + uid, next);
},
function (exists, next) {
if (exists) { if (exists) {
return callback(new Error('[[error:account-locked]]')); throw new Error('[[error:account-locked]]');
} }
db.increment('loginAttempts:' + uid, next); const attempts = await db.increment('loginAttempts:' + uid);
},
function (attempts, next) {
if (attempts <= meta.config.loginAttempts) { if (attempts <= meta.config.loginAttempts) {
return db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60, callback); return await db.pexpire('loginAttempts:' + uid, 1000 * 60 * 60);
} }
// Lock out the account // Lock out the account
db.set('lockout:' + uid, '', next); await db.set('lockout:' + uid, '');
},
function (next) {
var duration = 1000 * 60 * meta.config.lockoutDuration; var duration = 1000 * 60 * meta.config.lockoutDuration;
db.delete('loginAttempts:' + uid); await db.delete('loginAttempts:' + uid);
db.pexpire('lockout:' + uid, duration); await db.pexpire('lockout:' + uid, duration);
events.log({ events.log({
type: 'account-locked', type: 'account-locked',
uid: uid, uid: uid,
ip: ip, ip: ip,
}); });
next(new Error('[[error:account-locked]]')); throw new Error('[[error:account-locked]]');
},
], callback);
}; };
User.auth.getFeedToken = function (uid, callback) { User.auth.getFeedToken = async function (uid) {
if (parseInt(uid, 10) <= 0) { if (!(parseInt(uid, 10) > 0)) {
return setImmediate(callback); return;
} }
var token; var _token = await db.getObjectField('user:' + uid, 'rss_token');
async.waterfall([ const token = _token || utils.generateUUID();
function (next) {
db.getObjectField('user:' + uid, 'rss_token', next);
},
function (_token, next) {
token = _token || utils.generateUUID();
if (!_token) { if (!_token) {
User.setUserField(uid, 'rss_token', token, next); await User.setUserField(uid, 'rss_token', token);
} else {
next();
} }
}, return token;
function (next) {
next(null, token);
},
], callback);
}; };
User.auth.clearLoginAttempts = function (uid) { User.auth.clearLoginAttempts = async function (uid) {
db.delete('loginAttempts:' + uid); await db.delete('loginAttempts:' + uid);
}; };
User.auth.resetLockout = function (uid, callback) { User.auth.resetLockout = async function (uid) {
async.parallel([ await db.deleteAll([
async.apply(db.delete, 'loginAttempts:' + uid), 'loginAttempts:' + uid,
async.apply(db.delete, 'lockout:' + uid), 'lockout:' + uid,
], callback); ]);
}; };
User.auth.getSessions = function (uid, curSessionId, callback) { User.auth.getSessions = async function (uid, curSessionId) {
var _sids; const sids = await db.getSortedSetRevRange('uid:' + uid + ':sessions', 0, 19);
let sessions = await async.map(sids, db.sessionStore.get.bind(db.sessionStore));
// 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) { sessions.forEach(function (sessionObj, idx) {
if (sessionObj && sessionObj.meta) { if (sessionObj && sessionObj.meta) {
sessionObj.meta.current = curSessionId === _sids[idx]; sessionObj.meta.current = curSessionId === sids[idx];
} }
}); });
@ -114,19 +82,13 @@ module.exports = function (User) {
parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10);
if (expired) { if (expired) {
expiredSids.push(_sids[idx]); expiredSids.push(sids[idx]);
} }
return !expired; return !expired;
}); });
await Promise.all(expiredSids.map(s => User.auth.revokeSession(s, uid)));
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) { sessions = sessions.map(function (sessObj) {
if (sessObj.meta) { if (sessObj.meta) {
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
@ -134,85 +96,53 @@ module.exports = function (User) {
} }
return sessObj.meta; return sessObj.meta;
}).filter(Boolean); }).filter(Boolean);
next(null, sessions); return sessions;
},
], callback);
}; };
User.auth.addSession = function (uid, sessionId, callback) { User.auth.addSession = async function (uid, sessionId) {
callback = callback || function () {};
if (!(parseInt(uid, 10) > 0)) { 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) { const getSessionFromStore = util.promisify(function (sessionId, callback) {
winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid);
async.waterfall([
function (next) {
db.sessionStore.get(sessionId, function (err, sessionObj) { db.sessionStore.get(sessionId, function (err, sessionObj) {
next(err, sessionObj || null); callback(err, sessionObj || null);
}); });
}, });
function (sessionObj, next) {
async.parallel([ User.auth.revokeSession = async function (sessionId, uid) {
function (next) { winston.verbose('[user.auth] Revoking session ' + sessionId + ' for user ' + uid);
const sessionObj = await getSessionFromStore(sessionId);
if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) { if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) {
db.deleteObjectField('uid:' + uid + ':sessionUUID:sessionId', sessionObj.meta.uuid, next); await db.deleteObjectField('uid:' + uid + ':sessionUUID:sessionId', sessionObj.meta.uuid);
} else {
next();
} }
}, await async.parallel([
async.apply(db.sortedSetRemove, 'uid:' + uid + ':sessions', sessionId), async.apply(db.sortedSetRemove, 'uid:' + uid + ':sessions', sessionId),
async.apply(db.sessionStore.destroy.bind(db.sessionStore), sessionId), async.apply(db.sessionStore.destroy.bind(db.sessionStore), sessionId),
], function (err) { ]);
next(err);
});
},
], callback);
}; };
User.auth.revokeAllSessions = function (uid, callback) { User.auth.revokeAllSessions = async function (uid) {
async.waterfall([ const sids = await db.getSortedSetRange('uid:' + uid + ':sessions', 0, -1);
async.apply(db.getSortedSetRange, 'uid:' + uid + ':sessions', 0, -1), const promises = sids.map(s => User.auth.revokeSession(s, uid));
function (sids, next) { await Promise.all(promises);
async.each(sids, function (sid, next) {
User.auth.revokeSession(sid, uid, next);
}, next);
},
], callback);
}; };
User.auth.deleteAllSessions = function (callback) { User.auth.deleteAllSessions = async function () {
var _ = require('lodash'); await batch.processSortedSet('users:joindate', async function (uids) {
batch.processSortedSet('users:joindate', function (uids, next) { const sessionKeys = uids.map(uid => 'uid:' + uid + ':sessions');
var sessionKeys = uids.map(function (uid) { const sessionUUIDKeys = uids.map(uid => 'uid:' + uid + ':sessionUUID:sessionId');
return 'uid:' + uid + ':sessions'; const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1));
}); await async.parallel([
async.apply(db.deleteAll, sessionKeys.concat(sessionUUIDKeys)),
var sessionUUIDKeys = uids.map(function (uid) {
return 'uid:' + uid + ':sessionUUID:sessionId';
});
async.waterfall([
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) { function (next) {
async.each(sids, function (sid, next) { async.each(sids, function (sid, next) {
db.sessionStore.destroy(sid, next); db.sessionStore.destroy(sid, next);
}, next); }, next);
}, },
], next); ]);
}, }, { batch: 1000 });
], next);
}, { batch: 1000 }, callback);
}; };
}; };

Loading…
Cancel
Save