Skip to content

Commit

Permalink
feat: #8662, verified/unverified user groups
Browse files Browse the repository at this point in the history
  • Loading branch information
barisusakli committed Oct 13, 2020
1 parent 700e1e4 commit 682e926
Show file tree
Hide file tree
Showing 18 changed files with 172 additions and 62 deletions.
6 changes: 5 additions & 1 deletion src/controllers/admin/groups.js
Expand Up @@ -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) {
Expand Down
10 changes: 6 additions & 4 deletions src/controllers/admin/users.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/groups/create.js
Expand Up @@ -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);
}

Expand Down
8 changes: 8 additions & 0 deletions src/groups/index.js
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/groups/join.js
Expand Up @@ -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;
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/messaging/edit.js
Expand Up @@ -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');

Expand Down Expand Up @@ -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([
Expand Down
17 changes: 9 additions & 8 deletions src/messaging/index.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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', {
Expand Down
5 changes: 4 additions & 1 deletion src/privileges/helpers.js
Expand Up @@ -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 = {};

Expand All @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/socket.io/admin/user.js
Expand Up @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions src/upgrade.js
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
93 changes: 93 additions & 0 deletions src/upgrades/1.15.0/verified_users_group.js
@@ -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');
}
}
}
},
};
10 changes: 6 additions & 4 deletions src/user/create.js
Expand Up @@ -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]);
Expand All @@ -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),
Expand Down
1 change: 0 additions & 1 deletion src/user/delete.js
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion src/user/email.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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 });
};
10 changes: 3 additions & 7 deletions src/user/posts.js
Expand Up @@ -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),
]);

Expand All @@ -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 + ']]');
Expand Down
3 changes: 2 additions & 1 deletion src/user/profile.js
Expand Up @@ -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),
]);

Expand Down

0 comments on commit 682e926

Please sign in to comment.