mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: #8662, verified/unverified user groups
This commit is contained in:
@@ -67,7 +67,11 @@ groupsController.get = async function (req, res, next) {
|
||||
|
||||
async function getGroupNames() {
|
||||
const groupNames = await db.getSortedSetRange('groups:createtime', 0, -1);
|
||||
return groupNames.filter(name => name !== 'registered-users' && !groups.isPrivilegeGroup(name));
|
||||
return groupNames.filter(name => name !== 'registered-users' &&
|
||||
name !== 'verified-users' &&
|
||||
name !== 'unverified-users' &&
|
||||
!groups.isPrivilegeGroup(name)
|
||||
);
|
||||
}
|
||||
|
||||
groupsController.getCSV = async function (req, res) {
|
||||
|
||||
@@ -53,8 +53,11 @@ async function getUsers(req, res) {
|
||||
if (sortBy) {
|
||||
set.push(sortToSet[sortBy]);
|
||||
}
|
||||
if (filterBy.includes('notvalidated')) {
|
||||
set.push('users:notvalidated');
|
||||
if (filterBy.includes('unverified')) {
|
||||
set.push('group:unverified-users:members');
|
||||
}
|
||||
if (filterBy.includes('verified')) {
|
||||
set.push('group:verified-users:members');
|
||||
}
|
||||
if (filterBy.includes('banned')) {
|
||||
set.push('users:banned');
|
||||
@@ -219,9 +222,8 @@ async function getInvites() {
|
||||
|
||||
function render(req, res, data) {
|
||||
data.pagination = pagination.create(data.page, data.pageCount, req.query);
|
||||
data.requireEmailConfirmation = meta.config.requireEmailConfirmation;
|
||||
|
||||
var registrationType = meta.config.registrationType;
|
||||
const registrationType = meta.config.registrationType;
|
||||
|
||||
data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only';
|
||||
data.adminInviteOnly = registrationType === 'admin-invite-only';
|
||||
|
||||
@@ -67,7 +67,7 @@ module.exports = function (Groups) {
|
||||
|
||||
function isSystemGroup(data) {
|
||||
return data.system === true || parseInt(data.system, 10) === 1 ||
|
||||
data.name === 'administrators' || data.name === 'registered-users' || data.name === 'Global Moderators' ||
|
||||
Groups.systemGroups.includes(data.name) ||
|
||||
Groups.isPrivilegeGroup(data.name);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ require('./cache')(Groups);
|
||||
|
||||
Groups.ephemeralGroups = ['guests', 'spiders'];
|
||||
|
||||
Groups.systemGroups = [
|
||||
'registered-users',
|
||||
'verified-users',
|
||||
'unverified-users',
|
||||
'administrators',
|
||||
'Global Moderators',
|
||||
];
|
||||
|
||||
Groups.getEphemeralGroup = function (groupName) {
|
||||
return {
|
||||
name: groupName,
|
||||
|
||||
@@ -82,7 +82,7 @@ module.exports = function (Groups) {
|
||||
});
|
||||
} catch (err) {
|
||||
if (err && err.message !== '[[error:group-already-exists]]') {
|
||||
winston.error('[groups.join] Could not create new hidden group', err.stack);
|
||||
winston.error('[groups.join] Could not create new hidden group (' + groupName + ')\n' + err.stack);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const meta = require('../meta');
|
||||
const user = require('../user');
|
||||
const plugins = require('../plugins');
|
||||
const privileges = require('../privileges');
|
||||
|
||||
const sockets = require('../socket.io');
|
||||
|
||||
@@ -52,12 +53,13 @@ module.exports = function (Messaging) {
|
||||
throw new Error('[[error:chat-message-editing-disabled]]');
|
||||
}
|
||||
|
||||
const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']);
|
||||
const userData = await user.getUserFields(uid, ['banned']);
|
||||
if (userData.banned) {
|
||||
throw new Error('[[error:user-banned]]');
|
||||
}
|
||||
if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) {
|
||||
throw new Error('[[error:email-not-confirmed]]');
|
||||
const canChat = await privileges.global.can('chat', uid);
|
||||
if (!canChat) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const [isAdmin, messageData] = await Promise.all([
|
||||
|
||||
@@ -5,6 +5,7 @@ const validator = require('validator');
|
||||
|
||||
const db = require('../database');
|
||||
const user = require('../user');
|
||||
const privileges = require('../privileges');
|
||||
const plugins = require('../plugins');
|
||||
const meta = require('../meta');
|
||||
const utils = require('../utils');
|
||||
@@ -201,13 +202,13 @@ Messaging.canMessageUser = async (uid, toUid) => {
|
||||
throw new Error('[[error:no-user]]');
|
||||
}
|
||||
|
||||
const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']);
|
||||
const userData = await user.getUserFields(uid, ['banned']);
|
||||
if (userData.banned) {
|
||||
throw new Error('[[error:user-banned]]');
|
||||
}
|
||||
|
||||
if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) {
|
||||
throw new Error('[[error:email-not-confirmed-chat]]');
|
||||
const canChat = await privileges.global.can('chat', uid);
|
||||
if (!canChat) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const results = await utils.promiseParallel({
|
||||
@@ -237,13 +238,13 @@ Messaging.canMessageRoom = async (uid, roomId) => {
|
||||
throw new Error('[[error:not-in-room]]');
|
||||
}
|
||||
|
||||
const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']);
|
||||
const userData = await user.getUserFields(uid, ['banned']);
|
||||
if (userData.banned) {
|
||||
throw new Error('[[error:user-banned]]');
|
||||
}
|
||||
|
||||
if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) {
|
||||
throw new Error('[[error:email-not-confirmed-chat]]');
|
||||
const canChat = await privileges.global.can('chat', uid);
|
||||
if (!canChat) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
await plugins.fireHook('static:messaging.canMessageRoom', {
|
||||
|
||||
@@ -119,13 +119,15 @@ helpers.getGroupPrivileges = async function (cid, groupPrivileges) {
|
||||
|
||||
groupNames = groups.ephemeralGroups.concat(groupNames);
|
||||
moveToFront(groupNames, 'Global Moderators');
|
||||
moveToFront(groupNames, 'unverified-users');
|
||||
moveToFront(groupNames, 'verified-users');
|
||||
moveToFront(groupNames, 'registered-users');
|
||||
|
||||
const adminIndex = groupNames.indexOf('administrators');
|
||||
if (adminIndex !== -1) {
|
||||
groupNames.splice(adminIndex, 1);
|
||||
}
|
||||
const groupData = await groups.getGroupsFields(groupNames, ['private']);
|
||||
const groupData = await groups.getGroupsFields(groupNames, ['private', 'system']);
|
||||
const memberData = groupNames.map(function (member, index) {
|
||||
const memberPrivs = {};
|
||||
|
||||
@@ -137,6 +139,7 @@ helpers.getGroupPrivileges = async function (cid, groupPrivileges) {
|
||||
nameEscaped: translator.escape(validator.escape(member)),
|
||||
privileges: memberPrivs,
|
||||
isPrivate: groupData[index] && !!groupData[index].private,
|
||||
isSystem: groupData[index] && !!groupData[index].system,
|
||||
};
|
||||
});
|
||||
return memberData;
|
||||
|
||||
@@ -80,7 +80,10 @@ User.validateEmail = async function (socket, uids) {
|
||||
|
||||
uids = uids.filter(uid => parseInt(uid, 10));
|
||||
await db.setObjectField(uids.map(uid => 'user:' + uid), 'email:confirmed', 1);
|
||||
await db.sortedSetRemove('users:notvalidated', uids);
|
||||
for (const uid of uids) {
|
||||
await groups.join('verified-users', uid);
|
||||
await groups.leave('unverified-users', uid);
|
||||
}
|
||||
};
|
||||
|
||||
User.sendValidationEmail = async function (socket, uids) {
|
||||
|
||||
@@ -129,6 +129,7 @@ Upgrade.process = async function (files, skipCount) {
|
||||
const version = path.dirname(file).split('/').pop();
|
||||
const progress = {
|
||||
current: 0,
|
||||
counter: 0,
|
||||
total: 0,
|
||||
incr: Upgrade.incrementProgress,
|
||||
script: scriptExport,
|
||||
@@ -177,9 +178,11 @@ Upgrade.incrementProgress = function (value) {
|
||||
}
|
||||
|
||||
this.current += value || 1;
|
||||
this.counter += value || 1;
|
||||
const step = (this.total ? Math.floor(this.total / 100) : 100);
|
||||
|
||||
// Redraw the progress bar every 100 units
|
||||
if (this.current % (this.total ? Math.floor(this.total / 100) : 100) === 0 || this.current === this.total) {
|
||||
if (this.counter > step || this.current >= this.total) {
|
||||
this.counter -= step;
|
||||
var percentage = 0;
|
||||
var filled = 0;
|
||||
var unfilled = 15;
|
||||
|
||||
93
src/upgrades/1.15.0/verified_users_group.js
Normal file
93
src/upgrades/1.15.0/verified_users_group.js
Normal file
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../../database');
|
||||
|
||||
const batch = require('../../batch');
|
||||
const user = require('../../user');
|
||||
const groups = require('../../groups');
|
||||
const meta = require('../../meta');
|
||||
const privileges = require('../../privileges');
|
||||
|
||||
module.exports = {
|
||||
name: 'Create verified/unverified user groups',
|
||||
timestamp: Date.UTC(2020, 9, 13),
|
||||
method: async function () {
|
||||
const progress = this.progress;
|
||||
const timestamp = await db.getObjectField('group:administrators', 'timestamp');
|
||||
const verifiedExists = await groups.exists('verified-users');
|
||||
if (!verifiedExists) {
|
||||
await groups.create({
|
||||
name: 'verified-users',
|
||||
hidden: 1,
|
||||
private: 1,
|
||||
system: 1,
|
||||
disableLeave: 1,
|
||||
disableJoinRequests: 1,
|
||||
timestamp: timestamp + 1,
|
||||
});
|
||||
}
|
||||
const unverifiedExists = await groups.exists('unverified-users');
|
||||
if (!unverifiedExists) {
|
||||
await groups.create({
|
||||
name: 'unverified-users',
|
||||
hidden: 1,
|
||||
private: 1,
|
||||
system: 1,
|
||||
disableLeave: 1,
|
||||
disableJoinRequests: 1,
|
||||
timestamp: timestamp + 1,
|
||||
});
|
||||
}
|
||||
|
||||
await batch.processSortedSet('users:joindate', async function (uids) {
|
||||
progress.incr(uids.length);
|
||||
const userData = await user.getUsersFields(uids, ['uid', 'email:confirmed']);
|
||||
|
||||
const verified = userData.filter(u => parseInt(u['email:confirmed'], 10) === 1);
|
||||
const unverified = userData.filter(u => parseInt(u['email:confirmed'], 10) !== 1);
|
||||
|
||||
for (const user of verified) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await groups.join('verified-users', user.uid);
|
||||
}
|
||||
for (const user of unverified) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await groups.join('unverified-users', user.uid);
|
||||
}
|
||||
}, {
|
||||
batch: 500,
|
||||
progress: this.progress,
|
||||
});
|
||||
|
||||
await db.delete('users:notvalidated');
|
||||
|
||||
|
||||
const cids = await db.getSortedSetRevRange('categories:cid', 0, -1);
|
||||
const canChat = await privileges.global.canGroup('chat', 'registered-users');
|
||||
// if email confirmation is required
|
||||
// give chat, posting privs to "verified-users" group
|
||||
// remove chat, posting privs from "registered-users" group
|
||||
if (1 || meta.config.requireEmailConfirmation) {
|
||||
if (canChat) {
|
||||
await privileges.global.give(['groups:chat'], 'verified-users');
|
||||
await privileges.global.rescind(['groups:chat'], 'registered-users');
|
||||
}
|
||||
for (const cid of cids) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
const data = await privileges.categories.list(cid);
|
||||
|
||||
const registeredUsersPrivs = data.groups.find(d => d.name === 'registered-users').privileges;
|
||||
|
||||
if (registeredUsersPrivs['groups:topics:create']) {
|
||||
await privileges.categories.give(['groups:topics:create'], cid, 'verified-users');
|
||||
await privileges.categories.rescind(['groups:topics:create'], cid, 'registered-users');
|
||||
}
|
||||
|
||||
if (registeredUsersPrivs['groups:topics:reply']) {
|
||||
await privileges.categories.give(['groups:topics:reply'], cid, 'verified-users');
|
||||
await privileges.categories.rescind(['groups:topics:reply'], cid, 'registered-users');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -86,9 +86,6 @@ module.exports = function (User) {
|
||||
['users:reputation', 0, userData.uid],
|
||||
];
|
||||
|
||||
if (parseInt(userData.uid, 10) !== 1) {
|
||||
bulkAdd.push(['users:notvalidated', timestamp, userData.uid]);
|
||||
}
|
||||
if (userData.email) {
|
||||
bulkAdd.push(['email:uid', userData.uid, userData.email.toLowerCase()]);
|
||||
bulkAdd.push(['email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid]);
|
||||
@@ -99,10 +96,15 @@ module.exports = function (User) {
|
||||
bulkAdd.push(['fullname:sorted', 0, userData.fullname.toLowerCase() + ':' + userData.uid]);
|
||||
}
|
||||
|
||||
const groupsToJoin = ['registered-users'].concat(
|
||||
parseInt(userData.uid, 10) !== 1 ?
|
||||
'unverified-users' : 'verified-users'
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
db.incrObjectField('global', 'userCount'),
|
||||
db.sortedSetAddBulk(bulkAdd),
|
||||
groups.join('registered-users', userData.uid),
|
||||
groups.join(groupsToJoin, userData.uid),
|
||||
User.notifications.sendWelcomeNotification(userData.uid),
|
||||
storePassword(userData.uid, data.password),
|
||||
User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq),
|
||||
|
||||
@@ -81,7 +81,6 @@ module.exports = function (User) {
|
||||
'users:banned:expire',
|
||||
'users:flags',
|
||||
'users:online',
|
||||
'users:notvalidated',
|
||||
'digest:day:uids',
|
||||
'digest:week:uids',
|
||||
'digest:month:uids',
|
||||
|
||||
@@ -9,6 +9,7 @@ var plugins = require('../plugins');
|
||||
var db = require('../database');
|
||||
var meta = require('../meta');
|
||||
var emailer = require('../emailer');
|
||||
const groups = require('../groups');
|
||||
|
||||
var UserEmail = module.exports;
|
||||
|
||||
@@ -96,8 +97,9 @@ UserEmail.confirm = async function (code) {
|
||||
throw new Error('[[error:invalid-email]]');
|
||||
}
|
||||
await user.setUserField(confirmObj.uid, 'email:confirmed', 1);
|
||||
await groups.join('verified-users', confirmObj.uid);
|
||||
await groups.leave('unverified-users', confirmObj.uid);
|
||||
await db.delete('confirm:' + code);
|
||||
await db.delete('uid:' + confirmObj.uid + ':confirm:email:sent');
|
||||
await db.sortedSetRemove('users:notvalidated', confirmObj.uid);
|
||||
await plugins.fireHook('action:user.email.confirmed', { uid: confirmObj.uid, email: confirmObj.email });
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports = function (User) {
|
||||
return;
|
||||
}
|
||||
const [userData, isAdminOrMod] = await Promise.all([
|
||||
User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'email:confirmed', 'reputation'].concat([field])),
|
||||
User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'reputation'].concat([field])),
|
||||
privileges.categories.isAdminOrMod(cid, uid),
|
||||
]);
|
||||
|
||||
@@ -34,16 +34,12 @@ module.exports = function (User) {
|
||||
throw new Error('[[error:user-banned]]');
|
||||
}
|
||||
|
||||
if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) {
|
||||
throw new Error('[[error:email-not-confirmed]]');
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
const now = Date.now();
|
||||
if (now - userData.joindate < meta.config.initialPostDelay * 1000) {
|
||||
throw new Error('[[error:user-too-new, ' + meta.config.initialPostDelay + ']]');
|
||||
}
|
||||
|
||||
var lasttime = userData[field] || 0;
|
||||
const lasttime = userData[field] || 0;
|
||||
|
||||
if (meta.config.newbiePostDelay > 0 && meta.config.newbiePostDelayThreshold > userData.reputation && now - lasttime < meta.config.newbiePostDelay * 1000) {
|
||||
throw new Error('[[error:too-many-posts-newbie, ' + meta.config.newbiePostDelay + ', ' + meta.config.newbiePostDelayThreshold + ']]');
|
||||
|
||||
@@ -234,9 +234,10 @@ module.exports = function (User) {
|
||||
['email:uid', uid, newEmail.toLowerCase()],
|
||||
['email:sorted', 0, newEmail.toLowerCase() + ':' + uid],
|
||||
['user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()],
|
||||
['users:notvalidated', Date.now(), uid],
|
||||
]),
|
||||
User.setUserFields(uid, { email: newEmail, 'email:confirmed': 0 }),
|
||||
groups.leave('verified-users', uid),
|
||||
groups.join('unverified-users', uid),
|
||||
User.reset.cleanByUid(uid),
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ var nconf = require('nconf');
|
||||
var winston = require('winston');
|
||||
|
||||
var user = require('./index');
|
||||
const groups = require('../groups');
|
||||
var utils = require('../utils');
|
||||
var batch = require('../batch');
|
||||
|
||||
@@ -70,11 +71,12 @@ UserReset.commit = async function (code, password) {
|
||||
const hash = await user.hashPassword(password);
|
||||
|
||||
await user.setUserFields(uid, { password: hash, 'email:confirmed': 1 });
|
||||
await groups.join('verified-users', uid);
|
||||
await groups.leave('unverified-users', uid);
|
||||
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);
|
||||
|
||||
39
test/user.js
39
test/user.js
@@ -2073,34 +2073,23 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should confirm email of user', function (done) {
|
||||
var email = 'confirm@me.com';
|
||||
User.create({
|
||||
it('should confirm email of user', async function () {
|
||||
const email = 'confirm@me.com';
|
||||
const uid = await User.create({
|
||||
username: 'confirme',
|
||||
email: email,
|
||||
}, function (err, uid) {
|
||||
assert.ifError(err);
|
||||
User.email.sendValidationEmail(uid, email, function (err, code) {
|
||||
assert.ifError(err);
|
||||
User.email.confirm(code, function (err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
||||
async.parallel({
|
||||
confirmed: function (next) {
|
||||
db.getObjectField('user:' + uid, 'email:confirmed', next);
|
||||
},
|
||||
isMember: function (next) {
|
||||
db.isSortedSetMember('users:notvalidated', uid, next);
|
||||
},
|
||||
}, function (err, results) {
|
||||
assert.ifError(err);
|
||||
assert.equal(results.confirmed, 1);
|
||||
assert.equal(results.isMember, false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
const code = await User.email.sendValidationEmail(uid, email);
|
||||
const unverified = await groups.isMember(uid, 'unverified-users');
|
||||
assert.strictEqual(unverified, true);
|
||||
await User.email.confirm(code);
|
||||
const [confirmed, isVerified] = await Promise.all([
|
||||
db.getObjectField('user:' + uid, 'email:confirmed'),
|
||||
groups.isMember(uid, 'verified-users', uid),
|
||||
]);
|
||||
assert.strictEqual(parseInt(confirmed, 10), 1);
|
||||
assert.strictEqual(isVerified, true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user