mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: invites regardless of registration type, invite privilege, groups to join on acceptance (#8786)
* feat: allow invites in normal registration mode + invite privilege * feat: select groups to join from an invite * test: check if groups from invitations have been joined * fix: remove unused variable * feat: write API versions of socket calls * docs: openapi specs for the new routes * test: iron out mongo redis difference * refactor: move inviteGroups endpoint into write API * refactor: use GET /api/v3/users/:uid/invites/groups Instead of GET /api/v3/users/:uid/inviteGroups * fix: no need for /api/v3 prefix when using api module * fix: tests * refactor: change POST /api/v3/users/invite To POST /api/v3/users/:uid/invites * refactor: make helpers.invite awaitable * fix: restrict invite API to self-use only * fix: move invite groups controller to write api, +tests * fix: tests Co-authored-by: Julian Lam <julian@nodebb.org>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"upload-files": "Upload Files",
|
||||
"signature": "Signature",
|
||||
"ban": "Ban",
|
||||
"invite": "Invite",
|
||||
"search-content": "Search Content",
|
||||
"search-users": "Search Users",
|
||||
"search-tags": "Search Tags",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"online-only": "Online only",
|
||||
"invite": "Invite",
|
||||
"prompt-email": "Emails:",
|
||||
"groups-to-join": "Groups to be joined when invite is accepted:",
|
||||
"invitation-email-sent": "An invitation email has been sent to %1",
|
||||
"user_list": "User List",
|
||||
"recent_topics": "Recent Topics",
|
||||
|
||||
@@ -27,6 +27,8 @@ get:
|
||||
type: string
|
||||
sort_lastonline:
|
||||
type: boolean
|
||||
showInviteButton:
|
||||
type: boolean
|
||||
inviteOnly:
|
||||
type: boolean
|
||||
adminInviteOnly:
|
||||
|
||||
@@ -44,6 +44,10 @@ paths:
|
||||
$ref: 'write/users/uid/tokens/token.yaml'
|
||||
/users/{uid}/sessions/{uuid}:
|
||||
$ref: 'write/users/uid/sessions/uuid.yaml'
|
||||
/users/{uid}/invites:
|
||||
$ref: 'write/users/uid/invites.yaml'
|
||||
/users/{uid}/invites/groups:
|
||||
$ref: 'write/users/uid/invites/groups.yaml'
|
||||
/categories/:
|
||||
$ref: 'write/categories.yaml'
|
||||
/groups/:
|
||||
|
||||
48
public/openapi/write/users/uid/invites.yaml
Normal file
48
public/openapi/write/users/uid/invites.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
post:
|
||||
tags:
|
||||
- users
|
||||
summary: invite users with email by email
|
||||
description: This operation sends an invitation email to the given addresses, with an option to join selected groups on acceptance
|
||||
parameters:
|
||||
- in: path
|
||||
name: uid
|
||||
schema:
|
||||
type: integer
|
||||
required: true
|
||||
description: uid of the user sending invitations
|
||||
example: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
emails:
|
||||
type: string
|
||||
description: A single or list of comma separated email addresses
|
||||
example: friend01@example.com,friend02@example.com
|
||||
groupsToJoin:
|
||||
type: array
|
||||
description: A collection of group names
|
||||
example: ['administrators']
|
||||
required:
|
||||
- emails
|
||||
responses:
|
||||
'200':
|
||||
description: invitation email(s) sent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
'400':
|
||||
$ref: ../../../components/responses/400.yaml#/400
|
||||
'401':
|
||||
$ref: ../../../components/responses/401.yaml#/401
|
||||
'403':
|
||||
$ref: ../../../components/responses/403.yaml#/403
|
||||
23
public/openapi/write/users/uid/invites/groups.yaml
Normal file
23
public/openapi/write/users/uid/invites/groups.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
get:
|
||||
tags:
|
||||
- users
|
||||
summary: Get group names that the user can invite
|
||||
parameters:
|
||||
- in: path
|
||||
name: uid
|
||||
schema:
|
||||
type: integer
|
||||
required: true
|
||||
description: uid of the user to make the query for
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: A collection of group names returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
'401':
|
||||
$ref: ../../../../components/responses/401.yaml#/401
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
define('admin/manage/users', [
|
||||
'translator', 'benchpress', 'autocomplete', 'api', 'slugify',
|
||||
], function (translator, Benchpress, autocomplete, api, slugify) {
|
||||
'translator', 'benchpress', 'autocomplete', 'api', 'slugify', 'bootbox',
|
||||
], function (translator, Benchpress, autocomplete, api, slugify, bootbox) {
|
||||
var Users = {};
|
||||
|
||||
Users.init = function () {
|
||||
@@ -454,20 +454,55 @@ define('admin/manage/users', [
|
||||
}
|
||||
|
||||
function handleInvite() {
|
||||
$('[component="user/invite"]').on('click', function () {
|
||||
bootbox.prompt('[[admin/manage/users:alerts.prompt-email]]', function (email) {
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('user.invite', email, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
app.alertSuccess('[[admin/manage/users:alerts.email-sent-to, ' + email + ']]');
|
||||
$('[component="user/invite"]').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
api.get(`/api/v3/users/${app.user.uid}/invites/groups`, {}).then((groups) => {
|
||||
Benchpress.parse('modals/invite', { groups: groups }, function (html) {
|
||||
bootbox.dialog({
|
||||
message: html,
|
||||
title: '[[admin/manage/users:invite]]',
|
||||
onEscape: true,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: '[[admin/manage/users:alerts.button-cancel]]',
|
||||
className: 'btn-default',
|
||||
},
|
||||
invite: {
|
||||
label: '[[admin/manage/users:invite]]',
|
||||
className: 'btn-primary',
|
||||
callback: sendInvites,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
app.alertError(err.message);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvites() {
|
||||
var $emails = $('#invite-modal-emails');
|
||||
var $groups = $('#invite-modal-groups');
|
||||
|
||||
var data = {
|
||||
emails: $emails.val()
|
||||
.split(',')
|
||||
.map(m => m.trim())
|
||||
.filter(Boolean)
|
||||
.filter((m, i, arr) => i === arr.indexOf(m))
|
||||
.join(','),
|
||||
groupsToJoin: $groups.val(),
|
||||
};
|
||||
|
||||
if (!data.emails) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.post(`/users/${app.user.uid}/invites`, data).then(() => {
|
||||
app.alertSuccess('[[admin/manage/users:alerts.email-sent-to, ' + data.emails.replace(/,/g, ', ') + ']]');
|
||||
}).catch((err) => {
|
||||
app.alertError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
|
||||
define('forum/users', [
|
||||
'translator', 'benchpress', 'api',
|
||||
], function (translator, Benchpress, api) {
|
||||
'translator', 'benchpress', 'api', 'bootbox',
|
||||
], function (translator, Benchpress, api, bootbox) {
|
||||
var Users = {};
|
||||
|
||||
var searchTimeoutID = 0;
|
||||
@@ -136,21 +136,57 @@ define('forum/users', [
|
||||
}
|
||||
|
||||
function handleInvite() {
|
||||
$('[component="user/invite"]').on('click', function () {
|
||||
bootbox.prompt('[[users:prompt-email]]', function (email) {
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('user.invite', email, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
app.alertSuccess('[[users:invitation-email-sent, ' + email + ']]');
|
||||
$('[component="user/invite"]').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
api.get(`/api/v3/users/${app.user.uid}/invites/groups`, {}).then((groups) => {
|
||||
Benchpress.parse('modals/invite', { groups: groups }, function (html) {
|
||||
bootbox.dialog({
|
||||
message: html,
|
||||
title: '[[users:invite]]',
|
||||
onEscape: true,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: '[[modules:bootbox.cancel]]',
|
||||
className: 'btn-default',
|
||||
},
|
||||
invite: {
|
||||
label: '[[users:invite]]',
|
||||
className: 'btn-primary',
|
||||
callback: sendInvites,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
app.alertError(err.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvites() {
|
||||
var $emails = $('#invite-modal-emails');
|
||||
var $groups = $('#invite-modal-groups');
|
||||
|
||||
var data = {
|
||||
emails: $emails.val()
|
||||
.split(',')
|
||||
.map(m => m.trim())
|
||||
.filter(Boolean)
|
||||
.filter((m, i, arr) => i === arr.indexOf(m))
|
||||
.join(','),
|
||||
groupsToJoin: $groups.val(),
|
||||
};
|
||||
|
||||
if (!data.emails) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.post(`/users/${app.user.uid}/invites`, data).then(() => {
|
||||
app.alertSuccess('[[users:invitation-email-sent, ' + data.emails.replace(/,/g, ', ') + ']]');
|
||||
}).catch((err) => {
|
||||
app.alertError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
return Users;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ const db = require('../../database');
|
||||
const pagination = require('../../pagination');
|
||||
const events = require('../../events');
|
||||
const plugins = require('../../plugins');
|
||||
const privileges = require('../../privileges');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const usersController = module.exports;
|
||||
@@ -115,7 +116,7 @@ async function getUsers(req, res) {
|
||||
getUsersWithFields(set),
|
||||
]);
|
||||
|
||||
render(req, res, {
|
||||
await render(req, res, {
|
||||
users: users.filter(user => user && parseInt(user.uid, 10)),
|
||||
page: page,
|
||||
pageCount: Math.max(1, Math.ceil(count / resultsPerPage)),
|
||||
@@ -176,7 +177,7 @@ usersController.search = async function (req, res) {
|
||||
searchData.resultsPerPage = resultsPerPage;
|
||||
searchData.sortBy = req.query.sortBy;
|
||||
searchData.reverse = reverse;
|
||||
render(req, res, searchData);
|
||||
await render(req, res, searchData);
|
||||
};
|
||||
|
||||
usersController.registrationQueue = async function (req, res) {
|
||||
@@ -226,7 +227,7 @@ async function getInvites() {
|
||||
return invitations;
|
||||
}
|
||||
|
||||
function render(req, res, data) {
|
||||
async function render(req, res, data) {
|
||||
data.pagination = pagination.create(data.page, data.pageCount, req.query);
|
||||
|
||||
const registrationType = meta.config.registrationType;
|
||||
@@ -241,6 +242,12 @@ function render(req, res, data) {
|
||||
filterBy.forEach(function (filter) {
|
||||
data['filterBy_' + validator.escape(String(filter))] = true;
|
||||
});
|
||||
|
||||
data.showInviteButton = await privileges.users.hasInvitePrivilege(req.uid);
|
||||
if (data.adminInviteOnly) {
|
||||
data.showInviteButton = await privileges.users.isAdministrator(req.uid);
|
||||
}
|
||||
|
||||
res.render('admin/manage/users', data);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,11 @@ async function registerAndLoginUser(req, res, userData) {
|
||||
await authenticationController.doLogin(req, uid);
|
||||
}
|
||||
|
||||
user.deleteInvitationKey(userData.email);
|
||||
// Distinguish registrations through invites from direct ones
|
||||
if (userData.token) {
|
||||
await user.joinGroupsFromInvitation(uid, userData.email);
|
||||
}
|
||||
await user.deleteInvitationKey(userData.email);
|
||||
const referrer = req.body.referrer || req.session.referrer || nconf.get('relative_path') + '/';
|
||||
const complete = await plugins.fireHook('filter:register.complete', { uid: uid, referrer: referrer });
|
||||
req.session.returnTo = complete.referrer;
|
||||
@@ -74,7 +78,7 @@ authenticationController.register = async function (req, res) {
|
||||
|
||||
const userData = req.body;
|
||||
try {
|
||||
if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
|
||||
if (userData.token || registrationType === 'invite-only' || registrationType === 'admin-invite-only') {
|
||||
await user.verifyInvitation(userData);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ async function renderIfAdminOrGlobalMod(set, req, res) {
|
||||
|
||||
usersController.renderUsersPage = async function (set, req, res) {
|
||||
const userData = await usersController.getUsers(set, req.uid, req.query);
|
||||
render(req, res, userData);
|
||||
await render(req, res, userData);
|
||||
};
|
||||
|
||||
usersController.getUsers = async function (set, uid, query) {
|
||||
@@ -171,10 +171,15 @@ async function render(req, res, data) {
|
||||
data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only';
|
||||
data.adminInviteOnly = registrationType === 'admin-invite-only';
|
||||
data.invites = await user.getInvitesNumber(req.uid);
|
||||
data.showInviteButton = req.loggedIn && (
|
||||
(registrationType === 'invite-only' && (data.isAdmin || !data.maximumInvites || data.invites < data.maximumInvites)) ||
|
||||
(registrationType === 'admin-invite-only' && data.isAdmin)
|
||||
);
|
||||
|
||||
data.showInviteButton = false;
|
||||
if (data.adminInviteOnly) {
|
||||
data.showInviteButton = await privileges.users.isAdministrator(req.uid);
|
||||
} else if (req.loggedIn) {
|
||||
const canInvite = await privileges.users.hasInvitePrivilege(req.uid);
|
||||
data.showInviteButton = canInvite && (!data.maximumInvites || data.invites < data.maximumInvites);
|
||||
}
|
||||
|
||||
data['reputation:disabled'] = meta.config['reputation:disabled'];
|
||||
|
||||
res.append('X-Total-Count', data.userCount);
|
||||
|
||||
@@ -5,9 +5,10 @@ const nconf = require('nconf');
|
||||
|
||||
const db = require('../../database');
|
||||
const api = require('../../api');
|
||||
const user = require('../../user');
|
||||
const groups = require('../../groups');
|
||||
const meta = require('../../meta');
|
||||
const privileges = require('../../privileges');
|
||||
const user = require('../../user');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const helpers = require('../helpers');
|
||||
@@ -153,3 +154,60 @@ Users.revokeSession = async (req, res) => {
|
||||
await user.auth.revokeSession(_id, req.params.uid);
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Users.invite = async (req, res) => {
|
||||
const { emails, groupsToJoin = [] } = req.body;
|
||||
|
||||
if (!emails || !Array.isArray(groupsToJoin)) {
|
||||
return helpers.formatApiResponse(400, res, new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
// For simplicity, this API route is restricted to self-use only. This can change if needed.
|
||||
if (parseInt(req.user.uid, 10) !== parseInt(req.params.uid, 10)) {
|
||||
return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
const canInvite = await privileges.users.hasInvitePrivilege(req.uid);
|
||||
if (!canInvite) {
|
||||
return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
const registrationType = meta.config.registrationType;
|
||||
const isAdmin = await user.isAdministrator(req.uid);
|
||||
if (registrationType === 'admin-invite-only' && !isAdmin) {
|
||||
return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
const inviteGroups = await groups.getUserInviteGroups(req.uid);
|
||||
const cannotInvite = groupsToJoin.some(group => !inviteGroups.includes(group));
|
||||
if (groupsToJoin.length > 0 && cannotInvite) {
|
||||
return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
const max = meta.config.maximumInvites;
|
||||
const emailsArr = emails.split(',').map(email => email.trim()).filter(Boolean);
|
||||
|
||||
for (const email of emailsArr) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
let invites = 0;
|
||||
if (max) {
|
||||
invites = await user.getInvitesNumber(req.uid);
|
||||
}
|
||||
if (!isAdmin && max && invites >= max) {
|
||||
return helpers.formatApiResponse(403, res, new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]'));
|
||||
}
|
||||
|
||||
await user.sendInvitationEmail(req.uid, email, groupsToJoin);
|
||||
}
|
||||
|
||||
return helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Users.getInviteGroups = async function (req, res) {
|
||||
if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) {
|
||||
return helpers.formatApiResponse(401, res);
|
||||
}
|
||||
|
||||
const userInviteGroups = await groups.getUserInviteGroups(req.params.uid);
|
||||
return helpers.formatApiResponse(200, res, userInviteGroups);
|
||||
};
|
||||
|
||||
@@ -31,4 +31,34 @@ module.exports = function (Groups) {
|
||||
const isMembers = await Groups.isMemberOfGroups(uid, groupNames);
|
||||
return groupNames.filter((name, i) => isMembers[i]);
|
||||
}
|
||||
|
||||
Groups.getUserInviteGroups = async function (uid) {
|
||||
let allGroups = await Groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
|
||||
allGroups = allGroups.filter(group => !Groups.ephemeralGroups.includes(group.name));
|
||||
|
||||
const publicGroups = allGroups.filter(group => group.hidden === 0 && group.system === 0 && group.private === 0);
|
||||
const adminModGroups = [{ name: 'administrators' }, { name: 'Global Moderators' }];
|
||||
// Private (but not hidden)
|
||||
const privateGroups = allGroups.filter(group => group.hidden === 0 && group.system === 0 && group.private === 1);
|
||||
|
||||
const [ownership, isAdmin, isGlobalMod] = await Promise.all([
|
||||
Promise.all(privateGroups.map(group => Groups.ownership.isOwner(uid, group.name))),
|
||||
user.isAdministrator(uid),
|
||||
user.isGlobalModerator(uid),
|
||||
]);
|
||||
const ownGroups = privateGroups.filter((group, index) => ownership[index]);
|
||||
|
||||
let inviteGroups = [];
|
||||
if (isAdmin) {
|
||||
inviteGroups = inviteGroups.concat(adminModGroups).concat(privateGroups);
|
||||
} else if (isGlobalMod) {
|
||||
inviteGroups = inviteGroups.concat(privateGroups);
|
||||
} else {
|
||||
inviteGroups = inviteGroups.concat(ownGroups);
|
||||
}
|
||||
|
||||
return inviteGroups
|
||||
.concat(publicGroups)
|
||||
.map(group => group.name);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ module.exports = function (privileges) {
|
||||
{ name: '[[admin/manage/privileges:upload-files]]' },
|
||||
{ name: '[[admin/manage/privileges:signature]]' },
|
||||
{ name: '[[admin/manage/privileges:ban]]' },
|
||||
{ name: '[[admin/manage/privileges:invite]]' },
|
||||
{ name: '[[admin/manage/privileges:search-content]]' },
|
||||
{ name: '[[admin/manage/privileges:search-users]]' },
|
||||
{ name: '[[admin/manage/privileges:search-tags]]' },
|
||||
@@ -35,6 +36,7 @@ module.exports = function (privileges) {
|
||||
'upload:post:file',
|
||||
'signature',
|
||||
'ban',
|
||||
'invite',
|
||||
'search:content',
|
||||
'search:users',
|
||||
'search:tags',
|
||||
|
||||
@@ -115,4 +115,13 @@ module.exports = function (privileges) {
|
||||
});
|
||||
return data.canBan;
|
||||
};
|
||||
|
||||
privileges.users.hasInvitePrivilege = async function (uid) {
|
||||
const canInvite = await privileges.global.can('invite', uid);
|
||||
const data = await plugins.fireHook('filter:user.hasInvitePrivilege', {
|
||||
uid: uid,
|
||||
canInvite: canInvite,
|
||||
});
|
||||
return data.canInvite;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,6 +39,9 @@ function authenticatedRoutes() {
|
||||
|
||||
// Shorthand route to access user routes by userslug
|
||||
router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug);
|
||||
|
||||
setupApiRoute(router, 'post', '/:uid/invites', middlewares, controllers.write.users.invite);
|
||||
setupApiRoute(router, 'get', '/:uid/invites/groups', [...middlewares, middleware.assert.user], controllers.write.users.getInviteGroups);
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
const util = require('util');
|
||||
const sleep = util.promisify(setTimeout);
|
||||
|
||||
@@ -223,37 +221,6 @@ SocketUser.getUnreadCounts = async function (socket) {
|
||||
return results;
|
||||
};
|
||||
|
||||
SocketUser.invite = async function (socket, email) {
|
||||
if (!email || !socket.uid) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const registrationType = meta.config.registrationType;
|
||||
if (registrationType !== 'invite-only' && registrationType !== 'admin-invite-only') {
|
||||
throw new Error('[[error:forum-not-invite-only]]');
|
||||
}
|
||||
|
||||
const isAdmin = await user.isAdministrator(socket.uid);
|
||||
if (registrationType === 'admin-invite-only' && !isAdmin) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const max = meta.config.maximumInvites;
|
||||
email = email.split(',').map(email => email.trim()).filter(Boolean);
|
||||
|
||||
await async.eachSeries(email, async function (email) {
|
||||
let invites = 0;
|
||||
if (max) {
|
||||
invites = await user.getInvitesNumber(socket.uid);
|
||||
}
|
||||
if (!isAdmin && max && invites >= max) {
|
||||
throw new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]');
|
||||
}
|
||||
|
||||
await user.sendInvitationEmail(socket.uid, email);
|
||||
});
|
||||
};
|
||||
|
||||
SocketUser.getUserByUID = async function (socket, uid) {
|
||||
return await userController.getUserDataByField(socket.uid, 'uid', uid);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ var validator = require('validator');
|
||||
var db = require('../database');
|
||||
var meta = require('../meta');
|
||||
var emailer = require('../emailer');
|
||||
var groups = require('../groups');
|
||||
var translator = require('../translator');
|
||||
var utils = require('../utils');
|
||||
|
||||
@@ -36,13 +37,7 @@ module.exports = function (User) {
|
||||
});
|
||||
};
|
||||
|
||||
User.sendInvitationEmail = async function (uid, email) {
|
||||
const token = utils.generateUUID();
|
||||
const registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + encodeURIComponent(email);
|
||||
|
||||
const expireDays = meta.config.inviteExpiration;
|
||||
const expireIn = expireDays * 86400000;
|
||||
|
||||
User.sendInvitationEmail = async function (uid, email, groupsToJoin) {
|
||||
const email_exists = await User.getUidByEmail(email);
|
||||
if (email_exists) {
|
||||
throw new Error('[[error:email-taken]]');
|
||||
@@ -53,24 +48,7 @@ module.exports = function (User) {
|
||||
throw new Error('[[error:email-invited]]');
|
||||
}
|
||||
|
||||
await db.setAdd('invitation:uid:' + uid, email);
|
||||
await db.setAdd('invitation:uids', uid);
|
||||
await db.set('invitation:email:' + email, token);
|
||||
await db.pexpireAt('invitation:email:' + email, Date.now() + expireIn);
|
||||
const username = await User.getUserField(uid, 'username');
|
||||
const title = meta.config.title || meta.config.browserTitle || 'NodeBB';
|
||||
const subject = await translator.translate('[[email:invite, ' + title + ']]', meta.config.defaultLang);
|
||||
let data = {
|
||||
site_title: title,
|
||||
registerLink: registerLink,
|
||||
subject: subject,
|
||||
username: username,
|
||||
template: 'invitation',
|
||||
expireDays: expireDays,
|
||||
};
|
||||
|
||||
// Append default data to this email payload
|
||||
data = { ...emailer._defaultPayload, ...data };
|
||||
const data = await prepareInvitation(uid, email, groupsToJoin);
|
||||
|
||||
await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data);
|
||||
};
|
||||
@@ -79,12 +57,28 @@ module.exports = function (User) {
|
||||
if (!query.token || !query.email) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const token = await db.get('invitation:email:' + query.email);
|
||||
const token = await db.getObjectField('invitation:email:' + query.email, 'token');
|
||||
if (!token || token !== query.token) {
|
||||
throw new Error('[[error:invalid-token]]');
|
||||
}
|
||||
};
|
||||
|
||||
User.joinGroupsFromInvitation = async function (uid, email) {
|
||||
let groupsToJoin = await db.getObjectField('invitation:email:' + email, 'groupsToJoin');
|
||||
|
||||
try {
|
||||
groupsToJoin = JSON.parse(groupsToJoin);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!groupsToJoin || groupsToJoin.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
await groups.join(groupsToJoin, uid);
|
||||
};
|
||||
|
||||
User.deleteInvitation = async function (invitedBy, email) {
|
||||
const invitedByUid = await User.getUidByUsername(invitedBy);
|
||||
if (!invitedByUid) {
|
||||
@@ -109,4 +103,34 @@ module.exports = function (User) {
|
||||
await db.setRemove('invitation:uids', uid);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareInvitation(uid, email, groupsToJoin) {
|
||||
const token = utils.generateUUID();
|
||||
const registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + encodeURIComponent(email);
|
||||
|
||||
const expireDays = meta.config.inviteExpiration;
|
||||
const expireIn = expireDays * 86400000;
|
||||
|
||||
await db.setAdd('invitation:uid:' + uid, email);
|
||||
await db.setAdd('invitation:uids', uid);
|
||||
await db.setObject('invitation:email:' + email, {
|
||||
token,
|
||||
groupsToJoin: JSON.stringify(groupsToJoin),
|
||||
});
|
||||
await db.pexpireAt('invitation:email:' + email, Date.now() + expireIn);
|
||||
|
||||
const username = await User.getUserField(uid, 'username');
|
||||
const title = meta.config.title || meta.config.browserTitle || 'NodeBB';
|
||||
const subject = await translator.translate('[[email:invite, ' + title + ']]', meta.config.defaultLang);
|
||||
|
||||
return {
|
||||
...emailer._defaultPayload, // Append default data to this email payload
|
||||
site_title: title,
|
||||
registerLink: registerLink,
|
||||
subject: subject,
|
||||
username: username,
|
||||
template: 'invitation',
|
||||
expireDays: expireDays,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<div class="clearfix">
|
||||
|
||||
<div class="pull-left">
|
||||
<!-- IF inviteOnly -->
|
||||
<!-- IF showInviteButton -->
|
||||
<button component="user/invite" class="btn btn-success"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button>
|
||||
<!-- ENDIF inviteOnly -->
|
||||
<!-- ENDIF showInviteButton -->
|
||||
<a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary">[[admin/manage/users:download-csv]]</a>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button">[[admin/manage/users:edit]] <span class="caret"></span></button>
|
||||
|
||||
12
src/views/modals/invite.tpl
Normal file
12
src/views/modals/invite.tpl
Normal file
@@ -0,0 +1,12 @@
|
||||
<div class="form-group">
|
||||
<label for="invite-modal-emails">[[users:prompt-email]]</label>
|
||||
<input id="invite-modal-emails" type="text" class="form-control" placeholder="friend1@example.com,friend2@example.com" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="invite-modal-groups">[[users:groups-to-join]]</label>
|
||||
<select id="invite-modal-groups" class="form-control" multiple size="5">
|
||||
<!-- BEGIN groups -->
|
||||
<option value="{@value}">{@value}</option>
|
||||
<!-- END groups -->
|
||||
</select>
|
||||
</div>
|
||||
@@ -763,6 +763,7 @@ describe('Categories', function () {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(data, {
|
||||
ban: false,
|
||||
invite: false,
|
||||
chat: false,
|
||||
'search:content': false,
|
||||
'search:users': false,
|
||||
@@ -812,6 +813,7 @@ describe('Categories', function () {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(data, {
|
||||
'groups:ban': false,
|
||||
'groups:invite': false,
|
||||
'groups:chat': true,
|
||||
'groups:search:content': true,
|
||||
'groups:search:users': true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var request = require('request');
|
||||
const requestAsync = require('request-promise-native');
|
||||
var nconf = require('nconf');
|
||||
var fs = require('fs');
|
||||
var winston = require('winston');
|
||||
@@ -162,4 +163,20 @@ helpers.copyFile = function (source, target, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
helpers.invite = async function (body, uid, jar, csrf_token) {
|
||||
const res = await requestAsync.post(`${nconf.get('url')}/api/v3/users/${uid}/invites`, {
|
||||
jar: jar,
|
||||
// using "form" since client "api" module make requests with "application/x-www-form-urlencoded" content-type
|
||||
form: body,
|
||||
headers: {
|
||||
'x-csrf-token': csrf_token,
|
||||
},
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
|
||||
res.body = JSON.parse(res.body);
|
||||
return { res, body };
|
||||
};
|
||||
|
||||
require('../../src/promisify')(helpers);
|
||||
|
||||
457
test/user.js
457
test/user.js
@@ -5,6 +5,7 @@ var async = require('async');
|
||||
var path = require('path');
|
||||
var nconf = require('nconf');
|
||||
var request = require('request');
|
||||
const requestAsync = require('request-promise-native');
|
||||
var jwt = require('jsonwebtoken');
|
||||
|
||||
var db = require('./mocks/databasemock');
|
||||
@@ -1919,160 +1920,374 @@ describe('User', function () {
|
||||
});
|
||||
|
||||
describe('invites', function () {
|
||||
var socketUser = require('../src/socket.io/user');
|
||||
var notAnInviterUid;
|
||||
var inviterUid;
|
||||
var adminUid;
|
||||
|
||||
var PUBLIC_GROUP = 'publicGroup';
|
||||
var PRIVATE_GROUP = 'privateGroup';
|
||||
var OWN_PRIVATE_GROUP = 'ownPrivateGroup';
|
||||
var HIDDEN_GROUP = 'hiddenGroup';
|
||||
|
||||
var COMMON_PW = '123456';
|
||||
|
||||
before(function (done) {
|
||||
async.parallel({
|
||||
inviter: async.apply(User.create, { username: 'inviter', email: 'inviter@nodebb.org' }),
|
||||
admin: async.apply(User.create, { username: 'adminInvite' }),
|
||||
publicGroup: async.apply(groups.create, { name: PUBLIC_GROUP, private: 0 }),
|
||||
privateGroup: async.apply(groups.create, { name: PRIVATE_GROUP, private: 1 }),
|
||||
hiddenGroup: async.apply(groups.create, { name: HIDDEN_GROUP, hidden: 1 }),
|
||||
notAnInviter: async.apply(User.create, { username: 'notAnInviter', password: COMMON_PW, email: 'notaninviter@nodebb.org' }),
|
||||
inviter: async.apply(User.create, { username: 'inviter', password: COMMON_PW, email: 'inviter@nodebb.org' }),
|
||||
admin: async.apply(User.create, { username: 'adminInvite', password: COMMON_PW }),
|
||||
}, function (err, results) {
|
||||
assert.ifError(err);
|
||||
notAnInviterUid = results.notAnInviter;
|
||||
inviterUid = results.inviter;
|
||||
adminUid = results.admin;
|
||||
groups.join('administrators', adminUid, done);
|
||||
async.parallel([
|
||||
async.apply(groups.create, { name: OWN_PRIVATE_GROUP, ownerUid: inviterUid, private: 1 }),
|
||||
async.apply(groups.join, 'administrators', adminUid),
|
||||
async.apply(groups.join, 'cid:0:privileges:invite', inviterUid),
|
||||
], done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should error with invalid data', function (done) {
|
||||
socketUser.invite({ uid: inviterUid }, null, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-data]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe('when inviter is not an admin and does not have invite privilege', function () {
|
||||
var csrf_token;
|
||||
var jar;
|
||||
|
||||
it('should eror if forum is not invite only', function (done) {
|
||||
socketUser.invite({ uid: inviterUid }, 'invite1@test.com', function (err) {
|
||||
assert.equal(err.message, '[[error:forum-not-invite-only]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user is not admin and type is admin-invite-only', function (done) {
|
||||
meta.config.registrationType = 'admin-invite-only';
|
||||
socketUser.invite({ uid: inviterUid }, 'invite1@test.com', function (err) {
|
||||
assert.equal(err.message, '[[error:no-privileges]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send invitation email', function (done) {
|
||||
meta.config.registrationType = 'invite-only';
|
||||
socketUser.invite({ uid: inviterUid }, 'invite1@test.com', function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if ouf of invitations', function (done) {
|
||||
meta.config.maximumInvites = 1;
|
||||
socketUser.invite({ uid: inviterUid }, 'invite2@test.com', function (err) {
|
||||
assert.equal(err.message, '[[error:invite-maximum-met, ' + 1 + ', ' + 1 + ']]');
|
||||
meta.config.maximumInvites = 5;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if email exists', function (done) {
|
||||
socketUser.invite({ uid: inviterUid }, 'inviter@nodebb.org', function (err) {
|
||||
assert.equal(err.message, '[[error:email-taken]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should send invitation email', function (done) {
|
||||
socketUser.invite({ uid: inviterUid }, 'invite2@test.com', function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get user\'s invites', function (done) {
|
||||
User.getInvites(inviterUid, function (err, data) {
|
||||
assert.ifError(err);
|
||||
assert.notEqual(data.indexOf('invite1@test.com'), -1);
|
||||
assert.notEqual(data.indexOf('invite2@test.com'), -1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get all invites', function (done) {
|
||||
User.getAllInvites(function (err, data) {
|
||||
assert.ifError(err);
|
||||
assert.equal(data[0].uid, inviterUid);
|
||||
assert.notEqual(data[0].invitations.indexOf('invite1@test.com'), -1);
|
||||
assert.notEqual(data[0].invitations.indexOf('invite2@test.com'), -1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to verify invitation with invalid data', function (done) {
|
||||
User.verifyInvitation({ token: '', email: '' }, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-data]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to verify invitation with invalid email', function (done) {
|
||||
User.verifyInvitation({ token: 'test', email: 'doesnotexist@test.com' }, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-token]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should verify installation with no errors', function (done) {
|
||||
var email = 'invite1@test.com';
|
||||
db.get('invitation:email:' + email, function (err, token) {
|
||||
assert.ifError(err);
|
||||
User.verifyInvitation({ token: token, email: 'invite1@test.com' }, function (err) {
|
||||
before(function (done) {
|
||||
helpers.loginUser('notAnInviter', COMMON_PW, function (err, _jar) {
|
||||
assert.ifError(err);
|
||||
jar = _jar;
|
||||
|
||||
request({
|
||||
url: nconf.get('url') + '/api/config',
|
||||
json: true,
|
||||
jar: jar,
|
||||
}, function (err, response, body) {
|
||||
assert.ifError(err);
|
||||
csrf_token = body.csrf_token;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user does not have invite privilege', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite1@test.com', groupsToJoin: [] }, notAnInviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:no-privileges]]');
|
||||
});
|
||||
|
||||
it('should error out if user tries to use an inviter\'s uid via the API', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite1@test.com', groupsToJoin: [] }, inviterUid, jar, csrf_token);
|
||||
const numInvites = await User.getInvitesNumber(inviterUid);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:no-privileges]]');
|
||||
assert.strictEqual(numInvites, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inviter has invite privilege', function () {
|
||||
var csrf_token;
|
||||
var jar;
|
||||
|
||||
before(function (done) {
|
||||
helpers.loginUser('inviter', COMMON_PW, function (err, _jar) {
|
||||
assert.ifError(err);
|
||||
jar = _jar;
|
||||
|
||||
request({
|
||||
url: nconf.get('url') + '/api/config',
|
||||
json: true,
|
||||
jar: jar,
|
||||
}, function (err, response, body) {
|
||||
assert.ifError(err);
|
||||
csrf_token = body.csrf_token;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should error with invalid data', async () => {
|
||||
const { res } = await helpers.invite({}, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 400);
|
||||
assert.strictEqual(res.body.status.message, '[[error:invalid-data]]');
|
||||
});
|
||||
|
||||
it('should error if user is not admin and type is admin-invite-only', async () => {
|
||||
meta.config.registrationType = 'admin-invite-only';
|
||||
const { res } = await helpers.invite({ emails: 'invite1@test.com', groupsToJoin: [] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:no-privileges]]');
|
||||
});
|
||||
|
||||
it('should send invitation email (without groups to be joined)', async () => {
|
||||
meta.config.registrationType = 'normal';
|
||||
const { res } = await helpers.invite({ emails: 'invite1@test.com', groupsToJoin: [] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
});
|
||||
|
||||
it('should send multiple invitation emails (with a public group to be joined)', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite2@test.com,invite3@test.com', groupsToJoin: [PUBLIC_GROUP] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
});
|
||||
|
||||
it('should error if the user has not permission to invite to the group', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite4@test.com', groupsToJoin: [PRIVATE_GROUP] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:no-privileges]]');
|
||||
});
|
||||
|
||||
it('should error if a non-admin tries to invite to the administrators group', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite4@test.com', groupsToJoin: ['administrators'] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:no-privileges]]');
|
||||
});
|
||||
|
||||
it('should to invite to own private group', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite4@test.com', groupsToJoin: [OWN_PRIVATE_GROUP] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
});
|
||||
|
||||
it('should to invite to multiple groups', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite5@test.com', groupsToJoin: [PUBLIC_GROUP, OWN_PRIVATE_GROUP] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
});
|
||||
|
||||
it('should error if tries to invite to hidden group', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite6@test.com', groupsToJoin: [HIDDEN_GROUP] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
});
|
||||
|
||||
it('should error if ouf of invitations', async () => {
|
||||
meta.config.maximumInvites = 1;
|
||||
const { res } = await helpers.invite({ emails: 'invite6@test.com', groupsToJoin: [] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:invite-maximum-met, ' + 5 + ', ' + 1 + ']]');
|
||||
meta.config.maximumInvites = 10;
|
||||
});
|
||||
|
||||
it('should send invitation email after maximumInvites increased', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite6@test.com', groupsToJoin: [] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
});
|
||||
|
||||
it('should error if invite is sent via API with a different UID', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'inviter@nodebb.org', groupsToJoin: [] }, adminUid, jar, csrf_token);
|
||||
const numInvites = await User.getInvitesNumber(adminUid);
|
||||
assert.strictEqual(res.statusCode, 403);
|
||||
assert.strictEqual(res.body.status.message, '[[error:no-privileges]]');
|
||||
assert.strictEqual(numInvites, 0);
|
||||
});
|
||||
|
||||
it('should error if email exists', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'inviter@nodebb.org', groupsToJoin: [] }, inviterUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 400);
|
||||
assert.strictEqual(res.body.status.message, '[[error:email-taken]]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inviter is an admin', function () {
|
||||
var csrf_token;
|
||||
var jar;
|
||||
|
||||
before(function (done) {
|
||||
helpers.loginUser('adminInvite', COMMON_PW, function (err, _jar) {
|
||||
assert.ifError(err);
|
||||
jar = _jar;
|
||||
|
||||
request({
|
||||
url: nconf.get('url') + '/api/config',
|
||||
json: true,
|
||||
jar: jar,
|
||||
}, function (err, response, body) {
|
||||
assert.ifError(err);
|
||||
csrf_token = body.csrf_token;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should escape email', async () => {
|
||||
await helpers.invite({ emails: '<script>alert("ok");</script>', groupsToJoin: [] }, adminUid, jar, csrf_token);
|
||||
const data = await User.getInvites(adminUid);
|
||||
assert.strictEqual(data[0], '<script>alert("ok");</script>');
|
||||
await User.deleteInvitationKey('<script>alert("ok");</script>');
|
||||
});
|
||||
|
||||
it('should invite to the administrators group if inviter is an admin', async () => {
|
||||
const { res } = await helpers.invite({ emails: 'invite99@test.com', groupsToJoin: ['administrators'] }, adminUid, jar, csrf_token);
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after invites checks', function () {
|
||||
it('should get user\'s invites', function (done) {
|
||||
User.getInvites(inviterUid, function (err, data) {
|
||||
assert.ifError(err);
|
||||
Array.from(Array(6)).forEach((_, i) => {
|
||||
assert.notEqual(data.indexOf('invite' + (i + 1) + '@test.com'), -1);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should error with invalid username', function (done) {
|
||||
User.deleteInvitation('doesnotexist', 'test@test.com', function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-username]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete invitation', function (done) {
|
||||
var socketUser = require('../src/socket.io/user');
|
||||
socketUser.deleteInvitation({ uid: adminUid }, { invitedBy: 'inviter', email: 'invite1@test.com' }, function (err) {
|
||||
assert.ifError(err);
|
||||
db.isSetMember('invitation:uid:' + inviterUid, 'invite1@test.com', function (err, isMember) {
|
||||
it('should get all invites', function (done) {
|
||||
User.getAllInvites(function (err, data) {
|
||||
assert.ifError(err);
|
||||
assert.equal(isMember, false);
|
||||
|
||||
var adminData = data.filter(d => parseInt(d.uid, 10) === adminUid)[0];
|
||||
assert.notEqual(adminData.invitations.indexOf('invite99@test.com'), -1);
|
||||
|
||||
var inviterData = data.filter(d => parseInt(d.uid, 10) === inviterUid)[0];
|
||||
Array.from(Array(6)).forEach((_, i) => {
|
||||
assert.notEqual(inviterData.invitations.indexOf('invite' + (i + 1) + '@test.com'), -1);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete invitation key', function (done) {
|
||||
User.deleteInvitationKey('invite2@test.com', function (err) {
|
||||
assert.ifError(err);
|
||||
db.isSetMember('invitation:uid:' + inviterUid, 'invite2@test.com', function (err, isMember) {
|
||||
it('should fail to verify invitation with invalid data', function (done) {
|
||||
User.verifyInvitation({ token: '', email: '' }, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-data]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to verify invitation with invalid email', function (done) {
|
||||
User.verifyInvitation({ token: 'test', email: 'doesnotexist@test.com' }, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-token]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should verify installation with no errors', function (done) {
|
||||
var email = 'invite1@test.com';
|
||||
db.getObjectField('invitation:email:' + email, 'token', function (err, token) {
|
||||
assert.ifError(err);
|
||||
assert.equal(isMember, false);
|
||||
db.isSetMember('invitation:uids', inviterUid, function (err, isMember) {
|
||||
User.verifyInvitation({ token: token, email: 'invite1@test.com' }, function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should error with invalid username', function (done) {
|
||||
User.deleteInvitation('doesnotexist', 'test@test.com', function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-username]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete invitation', function (done) {
|
||||
var socketUser = require('../src/socket.io/user');
|
||||
socketUser.deleteInvitation({ uid: adminUid }, { invitedBy: 'inviter', email: 'invite1@test.com' }, function (err) {
|
||||
assert.ifError(err);
|
||||
db.isSetMember('invitation:uid:' + inviterUid, 'invite1@test.com', function (err, isMember) {
|
||||
assert.ifError(err);
|
||||
assert.equal(isMember, false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete invitation key', function (done) {
|
||||
User.deleteInvitationKey('invite99@test.com', function (err) {
|
||||
assert.ifError(err);
|
||||
db.isSetMember('invitation:uid:' + adminUid, 'invite99@test.com', function (err, isMember) {
|
||||
assert.ifError(err);
|
||||
assert.equal(isMember, false);
|
||||
db.isSetMember('invitation:uids', adminUid, function (err, isMember) {
|
||||
assert.ifError(err);
|
||||
assert.equal(isMember, false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should joined the groups from invitation after registration', async function () {
|
||||
var email = 'invite5@test.com';
|
||||
var groupsToJoin = [PUBLIC_GROUP, OWN_PRIVATE_GROUP];
|
||||
var token = await db.getObjectField('invitation:email:' + email, 'token');
|
||||
|
||||
await new Promise(function (resolve, reject) {
|
||||
helpers.registerUser({
|
||||
username: 'invite5',
|
||||
password: '123456',
|
||||
'password-confirm': '123456',
|
||||
email: email,
|
||||
gdpr_consent: true,
|
||||
token: token,
|
||||
}, async function (err, jar, response, body) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
var memberships = await groups.isMemberOfGroups(body.uid, groupsToJoin);
|
||||
var joinedToAll = memberships.filter(Boolean);
|
||||
|
||||
if (joinedToAll.length !== groupsToJoin.length) {
|
||||
reject(new Error('Not joined to the groups'));
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should escape email', function (done) {
|
||||
socketUser.invite({ uid: inviterUid }, '<script>alert("ok");</script>', function (err) {
|
||||
assert.ifError(err);
|
||||
User.getInvites(inviterUid, function (err, data) {
|
||||
describe('invite groups', () => {
|
||||
var csrf_token;
|
||||
var jar;
|
||||
|
||||
before(function (done) {
|
||||
helpers.loginUser('inviter', COMMON_PW, function (err, _jar) {
|
||||
assert.ifError(err);
|
||||
assert.equal(data[0], '<script>alert("ok");</script>');
|
||||
done();
|
||||
jar = _jar;
|
||||
|
||||
request({
|
||||
url: nconf.get('url') + '/api/config',
|
||||
json: true,
|
||||
jar: jar,
|
||||
}, function (err, response, body) {
|
||||
assert.ifError(err);
|
||||
csrf_token = body.csrf_token;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a list of groups for adding to an invite', async () => {
|
||||
const body = await requestAsync({
|
||||
url: `${nconf.get('url')}/api/v3/users/${inviterUid}/invites/groups`,
|
||||
json: true,
|
||||
jar,
|
||||
});
|
||||
|
||||
assert(Array.isArray(body.response));
|
||||
assert.strictEqual(2, body.response.length);
|
||||
assert.deepStrictEqual(body.response, ['ownPrivateGroup', 'publicGroup']);
|
||||
});
|
||||
|
||||
it('should error out if you request invite groups for another uid', async () => {
|
||||
const res = await requestAsync({
|
||||
url: `${nconf.get('url')}/api/v3/users/${adminUid}/invites/groups`,
|
||||
json: true,
|
||||
jar,
|
||||
simple: false,
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
|
||||
assert.strictEqual(res.statusCode, 401);
|
||||
assert.deepStrictEqual(res.body, {
|
||||
status: {
|
||||
code: 'not-authorised',
|
||||
message: 'A valid login session was not found. Please log in and try again.',
|
||||
},
|
||||
response: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user