Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 167 additions & 156 deletions src/user/invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,176 +15,187 @@
const plugins = require('../plugins');

module.exports = function (User) {
User.getInvites = async function (uid) {
const emails = await db.getSetMembers(`invitation:uid:${uid}`);
return emails.map(email => validator.escape(String(email)));
};

User.getInvitesNumber = async function (uid) {
return await db.setCount(`invitation:uid:${uid}`);
};

User.getInvitingUsers = async function () {
return await db.getSetMembers('invitation:uids');
};
User.getInvites = uid => getInvites(uid);
User.getInvitesNumber = uid => getInvitesNumber(uid);
User.getInvitingUsers = () => getInvitingUsers();
User.getAllInvites = () => getAllInvites();
User.sendInvitationEmail = (uid, email, groupsToJoin) => sendInvitationEmail(User, uid, email, groupsToJoin);
User.verifyInvitation = query => verifyInvitation(query);
User.confirmIfInviteEmailIsUsed = (token, enteredEmail, uid) => confirmIfInviteEmailIsUsed(User, token, enteredEmail, uid);

Check failure on line 24 in src/user/invite.js

View workflow job for this annotation

GitHub Actions / test

This line has a length of 125. Maximum allowed is 120
User.joinGroupsFromInvitation = (uid, token) => joinGroupsFromInvitation(uid, token);
User.deleteInvitation = (invitedBy, email) => deleteInvitation(User, invitedBy, email);
User.deleteInvitationKey = (registrationEmail, token) => deleteInvitationKey(registrationEmail, token);
};

User.getAllInvites = async function () {
const uids = await User.getInvitingUsers();
const invitations = await async.map(uids, User.getInvites);
return invitations.map((invites, index) => ({
uid: uids[index],
invitations: invites,
}));
};
async function getInvites(uid) {
const emails = await db.getSetMembers(`invitation:uid:${uid}`);
return emails.map(email => validator.escape(String(email)));
}

async function getInvitesNumber(uid) {
return await db.setCount(`invitation:uid:${uid}`);
}

async function getInvitingUsers() {
return await db.getSetMembers('invitation:uids');
}

async function getAllInvites() {
const uids = await getInvitingUsers();
const invitations = await async.map(uids, getInvites);
return invitations.map((invites, index) => ({
uid: uids[index],
invitations: invites,
}));
}

async function sendInvitationEmail(User, uid, email, groupsToJoin) {
if (!uid) {
throw new Error('[[error:invalid-uid]]');
}

User.sendInvitationEmail = async function (uid, email, groupsToJoin) {
if (!uid) {
throw new Error('[[error:invalid-uid]]');
}
const email_exists = await User.getUidByEmail(email);
if (email_exists) {
// Silently drop the invitation if the invited email already exists locally
return true;
}

const email_exists = await User.getUidByEmail(email);
if (email_exists) {
// Silently drop the invitation if the invited email already exists locally
return true;
}
const invitation_exists = await db.exists(`invitation:uid:${uid}:invited:${email}`);
if (invitation_exists) {
throw new Error('[[error:email-invited]]');
}

const invitation_exists = await db.exists(`invitation:uid:${uid}:invited:${email}`);
if (invitation_exists) {
throw new Error('[[error:email-invited]]');
const data = await prepareInvitation(User, uid, email, groupsToJoin);
await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data);
plugins.hooks.fire('action:user.invite', { uid, email, groupsToJoin });
}

async function verifyInvitation(query) {
if (!query.token) {
if (meta.config.registrationType.startsWith('admin-')) {
throw new Error('[[register:invite.error-admin-only]]');
} else {
throw new Error('[[register:invite.error-invite-only]]');
}
}
const token = await db.getObjectField(`invitation:token:${query.token}`, 'token');
if (!token || token !== query.token) {
throw new Error('[[register:invite.error-invalid-data]]');
}
}

const data = await prepareInvitation(uid, email, groupsToJoin);
await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data);
plugins.hooks.fire('action:user.invite', { uid, email, groupsToJoin });
};
async function confirmIfInviteEmailIsUsed(User, token, enteredEmail, uid) {
if (!enteredEmail) {
return;
}
const email = await db.getObjectField(`invitation:token:${token}`, 'email');
// "Confirm" user's email if registration completed with invited address
if (email && email === enteredEmail) {
await User.setUserField(uid, 'email', email);
await User.email.confirmByUid(uid);
}
}

User.verifyInvitation = async function (query) {
if (!query.token) {
if (meta.config.registrationType.startsWith('admin-')) {
throw new Error('[[register:invite.error-admin-only]]');
} else {
throw new Error('[[register:invite.error-invite-only]]');
}
}
const token = await db.getObjectField(`invitation:token:${query.token}`, 'token');
if (!token || token !== query.token) {
throw new Error('[[register:invite.error-invalid-data]]');
}
};
async function joinGroupsFromInvitation(uid, token) {
let groupsToJoin = await db.getObjectField(`invitation:token:${token}`, 'groupsToJoin');

User.confirmIfInviteEmailIsUsed = async function (token, enteredEmail, uid) {
if (!enteredEmail) {
return;
}
const email = await db.getObjectField(`invitation:token:${token}`, 'email');
// "Confirm" user's email if registration completed with invited address
if (email && email === enteredEmail) {
await User.setUserField(uid, 'email', email);
await User.email.confirmByUid(uid);
}
};
try {
groupsToJoin = JSON.parse(groupsToJoin);
} catch (err) {
winston.error(`[User.joinGroupsFromInvitation] ${err.stack}`);
return;
}

User.joinGroupsFromInvitation = async function (uid, token) {
let groupsToJoin = await db.getObjectField(`invitation:token:${token}`, 'groupsToJoin');
if (!groupsToJoin || groupsToJoin.length < 1) {
return;
}

try {
groupsToJoin = JSON.parse(groupsToJoin);
} catch (err) {
winston.error(`[User.joinGroupsFromInvitation] ${err.stack}`);
return;
}
await groups.join(groupsToJoin, uid);
}

if (!groupsToJoin || groupsToJoin.length < 1) {
async function deleteInvitation(User, invitedBy, email) {
const invitedByUid = await User.getUidByUsername(invitedBy);
if (!invitedByUid) {
throw new Error('[[error:invalid-username]]');
}
const token = await db.get(`invitation:uid:${invitedByUid}:invited:${email}`);
await Promise.all([
deleteFromReferenceList(invitedByUid, email),
db.setRemove(`invitation:invited:${email}`, token),
db.delete(`invitation:token:${token}`),
]);
}

async function deleteInvitationKey(registrationEmail, token) {
if (registrationEmail) {
const uids = await getInvitingUsers();
await Promise.all(uids.map(uid => deleteFromReferenceList(uid, registrationEmail)));
// Delete all invites to an email address if it has joined
const tokens = await db.getSetMembers(`invitation:invited:${registrationEmail}`);
const keysToDelete = [`invitation:invited:${registrationEmail}`].concat(tokens.map(token => `invitation:token:${token}`));
await db.deleteAll(keysToDelete);
}
if (token) {
const invite = await db.getObject(`invitation:token:${token}`);
if (!invite) {
return;
}

await groups.join(groupsToJoin, uid);
};

User.deleteInvitation = async function (invitedBy, email) {
const invitedByUid = await User.getUidByUsername(invitedBy);
if (!invitedByUid) {
throw new Error('[[error:invalid-username]]');
}
const token = await db.get(`invitation:uid:${invitedByUid}:invited:${email}`);
await Promise.all([
deleteFromReferenceList(invitedByUid, email),
db.setRemove(`invitation:invited:${email}`, token),
db.delete(`invitation:token:${token}`),
]);
};

User.deleteInvitationKey = async function (registrationEmail, token) {
if (registrationEmail) {
const uids = await User.getInvitingUsers();
await Promise.all(uids.map(uid => deleteFromReferenceList(uid, registrationEmail)));
// Delete all invites to an email address if it has joined
const tokens = await db.getSetMembers(`invitation:invited:${registrationEmail}`);
const keysToDelete = [`invitation:invited:${registrationEmail}`].concat(tokens.map(token => `invitation:token:${token}`));
await db.deleteAll(keysToDelete);
}
if (token) {
const invite = await db.getObject(`invitation:token:${token}`);
if (!invite) {
return;
}
await deleteFromReferenceList(invite.inviter, invite.email);
await db.deleteAll([
`invitation:invited:${invite.email}`,
`invitation:token:${token}`,
]);
}
};

async function deleteFromReferenceList(uid, email) {
await Promise.all([
db.setRemove(`invitation:uid:${uid}`, email),
db.delete(`invitation:uid:${uid}:invited:${email}`),
await deleteFromReferenceList(invite.inviter, invite.email);
await db.deleteAll([
`invitation:invited:${invite.email}`,
`invitation:token:${token}`,
]);
const count = await db.setCount(`invitation:uid:${uid}`);
if (count === 0) {
await db.setRemove('invitation:uids', uid);
}
}
}

async function deleteFromReferenceList(uid, email) {
await Promise.all([
db.setRemove(`invitation:uid:${uid}`, email),
db.delete(`invitation:uid:${uid}:invited:${email}`),
]);
const count = await db.setCount(`invitation:uid:${uid}`);
if (count === 0) {
await db.setRemove('invitation:uids', uid);
}
}

async function prepareInvitation(uid, email, groupsToJoin) {
const inviterExists = await User.exists(uid);
if (!inviterExists) {
throw new Error('[[error:invalid-uid]]');
}

const token = utils.generateUUID();
const registerLink = `${nconf.get('url')}/register?token=${token}`;

const expireDays = meta.config.inviteExpiration;
const expireIn = expireDays * 86400000;

await db.setAdd(`invitation:uid:${uid}`, email);
await db.setAdd('invitation:uids', uid);
// Referencing from uid and email to token
await db.set(`invitation:uid:${uid}:invited:${email}`, token);
// Keeping references for all invites to this email address
await db.setAdd(`invitation:invited:${email}`, token);
await db.setObject(`invitation:token:${token}`, {
email,
token,
groupsToJoin: JSON.stringify(groupsToJoin),
inviter: uid,
});
await db.pexpireAt(`invitation:token:${token}`, 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,
};
async function prepareInvitation(User, uid, email, groupsToJoin) {
const inviterExists = await User.exists(uid);
if (!inviterExists) {
throw new Error('[[error:invalid-uid]]');
}
};

const token = utils.generateUUID();
const registerLink = `${nconf.get('url')}/register?token=${token}`;

const expireDays = meta.config.inviteExpiration;
const expireIn = expireDays * 86400000;

await db.setAdd(`invitation:uid:${uid}`, email);
await db.setAdd('invitation:uids', uid);
// Referencing from uid and email to token
await db.set(`invitation:uid:${uid}:invited:${email}`, token);
// Keeping references for all invites to this email address
await db.setAdd(`invitation:invited:${email}`, token);
await db.setObject(`invitation:token:${token}`, {
email,
token,
groupsToJoin: JSON.stringify(groupsToJoin),
inviter: uid,
});
await db.pexpireAt(`invitation:token:${token}`, 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,
};
}
Loading