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');
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']);

@ -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);

@ -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;
}
};

@ -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 });
};
};

Loading…
Cancel
Save