Skip to content
Merged
Show file tree
Hide file tree
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
14 changes: 12 additions & 2 deletions src/actions/organization/invites/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ const sendInviteMail = require('../../../utils/organization/sendInviteMail');
const getInternalData = require('../../../utils/organization/getInternalData');
const redisKey = require('../../../utils/key');
const { checkOrganizationExists } = require('../../../utils/organization');
const { ORGANIZATIONS_MEMBERS, ErrorUserNotMember, ORGANIZATIONS_NAME_FIELD } = require('../../../constants');
const {
ORGANIZATIONS_MEMBERS,
ErrorUserNotMember,
ORGANIZATIONS_NAME_FIELD,
ORGANIZATIONS_ID_FIELD,
USERS_ACTION_ORGANIZATION_INVITE,
} = require('../../../constants');

/**
* @api {amqp} <prefix>.invites.send Send invitation
Expand All @@ -30,13 +36,17 @@ async function sendOrganizationInvite({ params }) {
if (!userInOrganization) {
throw ErrorUserNotMember;
}
const organization = await getInternalData.call(this, organizationId, false);
const organization = await getInternalData.call(this, organizationId);

return sendInviteMail.call(this, {
email: member.email,
action: USERS_ACTION_ORGANIZATION_INVITE,
ctx: {
firstName: member.firstName,
lastName: member.lastName,
password: member.password,
email: member.email,
organizationId: organization[ORGANIZATIONS_ID_FIELD],
organization: organization[ORGANIZATIONS_NAME_FIELD],
},
});
Expand Down
3 changes: 2 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ module.exports = exports = {
USERS_ACTION_RESET: 'reset',
USERS_ACTION_REGISTER: 'register',
USERS_ACTION_INVITE: 'invite',
USERS_ACTION_ORGANIZATION_INVITE: 'organization-invite',
USERS_ACTION_ORGANIZATION_INVITE: 'organization-user-invite',
USERS_ACTION_ORGANIZATION_REGISTER: 'organization-user-register',

// invitations constants
INVITATIONS_INDEX: 'user-invitations',
Expand Down
8 changes: 7 additions & 1 deletion src/utils/challenges/email/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
USERS_ACTION_REGISTER,
USERS_ACTION_INVITE,
USERS_ACTION_ORGANIZATION_INVITE,
USERS_ACTION_ORGANIZATION_REGISTER,
} = require('../../../constants.js');

// will be replaced later
Expand All @@ -34,11 +35,16 @@ function generate(email, type, ctx = {}, opts = {}, nodemailer = {}) {
case USERS_ACTION_ACTIVATE:
case USERS_ACTION_RESET:
case USERS_ACTION_INVITE:
case USERS_ACTION_ORGANIZATION_INVITE:
// generate secret
context.qs = `?q=${context.token.secret}`;
context.link = generateLink(server, paths[type]);
break;
case USERS_ACTION_ORGANIZATION_INVITE:
case USERS_ACTION_ORGANIZATION_REGISTER:
// generate secret
context.qs = `?q=${context.token.secret}&organizationId=${ctx.organizationId}&username=${ctx.email}`;
context.link = generateLink(server, paths[type]);
break;

case USERS_ACTION_PASSWORD:
case USERS_ACTION_REGISTER:
Expand Down
39 changes: 34 additions & 5 deletions src/utils/organization/addOrganizationMembers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
/* eslint-disable no-mixed-operators */
const Promise = require('bluebird');
const redisKey = require('../key.js');
const getUserId = require('../userData/getUserId');
const sendInviteMail = require('./sendInviteMail');
const getInternalData = require('./getInternalData');
const registerOrganizationMembers = require('./registerOrganizationMembers');
const handlePipeline = require('../pipelineError.js');
const { ORGANIZATIONS_MEMBERS, USERS_ORGANIZATIONS, ORGANIZATIONS_NAME_FIELD } = require('../../constants.js');
const {
ORGANIZATIONS_MEMBERS,
USERS_ORGANIZATIONS,
ORGANIZATIONS_NAME_FIELD,
USERS_ACTION_ORGANIZATION_INVITE,
USERS_ACTION_ORGANIZATION_REGISTER,
ORGANIZATIONS_ID_FIELD,
} = require('../../constants.js');

/**
* Updates metadata on a organization object
Expand All @@ -15,31 +24,51 @@ async function addOrganizationMembers(opts) {
const { redis } = this;
const { organizationId, members } = opts;

const registeredMembers = [];
const notRegisteredMembers = [];

const filterMembersJob = members.map(async (member) => {
try {
const userId = await getUserId.call(this, member.email);
registeredMembers.push({ ...member, id: userId });
} catch (e) {
notRegisteredMembers.push(member);
}
});
await Promise.all(filterMembersJob);

const createdMembers = await registerOrganizationMembers.call(this, notRegisteredMembers);

const pipe = redis.pipeline();
const membersKey = redisKey(organizationId, ORGANIZATIONS_MEMBERS);
members.forEach((member) => {
const organizationMembers = registeredMembers.concat(createdMembers);
organizationMembers.forEach(({ password, ...member }) => {
const memberKey = redisKey(organizationId, ORGANIZATIONS_MEMBERS, member.email);
const memberOrganizations = redisKey(member.email, USERS_ORGANIZATIONS);
member.username = member.email;
member.invited = Date.now();
member.accepted = null;
member.accepted = password ? Date.now() : null;
member.permissions = member.permissions || [];
pipe.hmset(memberKey, member);
pipe.hset(memberOrganizations, organizationId, JSON.stringify(member.permissions));
pipe.zadd(membersKey, member.invited, memberKey);
});

await pipe.exec().then(handlePipeline);
const organization = await getInternalData.call(this, organizationId, false);
const organization = await getInternalData.call(this, organizationId);

const membersIdsJob = [];
for (const member of members) {
for (const member of organizationMembers) {
membersIdsJob.push(
sendInviteMail.call(this, {
email: member.email,
action: member.password ? USERS_ACTION_ORGANIZATION_REGISTER : USERS_ACTION_ORGANIZATION_INVITE,
ctx: {
firstName: member.firstName,
lastName: member.lastName,
password: member.password,
email: member.email,
organizationId: organization[ORGANIZATIONS_ID_FIELD],
organization: organization[ORGANIZATIONS_NAME_FIELD],
},
})
Expand Down
68 changes: 68 additions & 0 deletions src/utils/organization/registerOrganizationMembers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable no-mixed-operators */
const Promise = require('bluebird');
const generatePassword = require('password-generator');
const redisKey = require('../key.js');
const handlePipeline = require('../pipelineError.js');
const {
USERS_CREATED_FIELD,
USERS_USERNAME_FIELD,
USERS_ACTIVE_FLAG,
USERS_PASSWORD_FIELD,
USERS_DATA,
USERS_USERNAME_TO_ID,
USERS_INDEX,
USERS_ID_FIELD,
} = require('../../constants.js');
const hashPassword = require('../register/password/hash');
const setMetadata = require('../updateMetadata');

async function registerOrganizationMember(member) {
const { redis, config } = this;
const { pwdReset, organizations: { audience } } = config;
const { email } = member;

const userId = this.flake.next();
const pipeline = redis.pipeline();
const basicInfo = {
[USERS_CREATED_FIELD]: Date.now(),
[USERS_USERNAME_FIELD]: email,
[USERS_ACTIVE_FLAG]: true,
};
const password = generatePassword(pwdReset.length, pwdReset.memorable);
basicInfo[USERS_PASSWORD_FIELD] = await hashPassword.call(this, password);

const userDataKey = redisKey(userId, USERS_DATA);
pipeline.hmset(userDataKey, basicInfo);
pipeline.hset(USERS_USERNAME_TO_ID, email, userId);
await pipeline.exec().then(handlePipeline);

await setMetadata.call(this, {
userId,
audience,
metadata: [{
$set: {
[USERS_ID_FIELD]: userId,
[USERS_USERNAME_FIELD]: email,
[USERS_CREATED_FIELD]: basicInfo[USERS_CREATED_FIELD],
},
}],
});
// perform instant activation
// internal username index
const regPipeline = redis.pipeline().sadd(USERS_INDEX, userId);

return regPipeline
.exec()
.then(handlePipeline)
// custom actions
.bind(this)
.return(['users:activate', userId])
.spread(this.hook)
.return({ ...member, id: userId, password });
}

function registerOrganizationMembers(members) {
return Promise.all(members.map(member => registerOrganizationMember.call(this, member)));
}

module.exports = registerOrganizationMembers;
7 changes: 3 additions & 4 deletions src/utils/organization/sendInviteMail.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@ const {
INVITATIONS_INDEX,
TOKEN_METADATA_FIELD_CONTEXT,
TOKEN_METADATA_FIELD_SENDED_AT,
USERS_ACTION_ORGANIZATION_INVITE,
} = require('../../constants.js');

module.exports = function sendInviteMail(params) {
const { redis, tokenManager } = this;
const { email, ctx = {} } = params;
const { email, action, ctx = {} } = params;
const now = Date.now();

return tokenManager
.create({
id: email,
action: USERS_ACTION_ORGANIZATION_INVITE,
action,
regenerate: true,
metadata: {
[TOKEN_METADATA_FIELD_CONTEXT]: ctx,
[TOKEN_METADATA_FIELD_SENDED_AT]: now,
},
})
.then(token => Promise
.bind(this, [email, USERS_ACTION_ORGANIZATION_INVITE, { ...ctx, token }, { send: true }])
.bind(this, [email, action, { ...ctx, token }, { send: true }])
.spread(generateEmail)
.tap(() => redis.sadd(INVITATIONS_INDEX, email)));
};
3 changes: 2 additions & 1 deletion test/configs/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ module.exports = {
password: 'cpst-password',
register: 'cpst-register',
invite: 'rfx-invite',
'organization-invite': 'sl-accept-invite',
'organization-user-invite': 'sl-accept-invite',
'organization-user-register': 'sl-registration-notify',
},
},
registrationLimits: {
Expand Down
8 changes: 7 additions & 1 deletion test/helpers/organization.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const faker = require('faker');
const Promise = require('bluebird');
const times = require('lodash/times');
const { inspectPromise } = require('@makeomatic/deploy');

async function createMembers(totalUsers = 1) {
async function createMembers(totalUsers = 1, register = false) {
this.userNames = [];

times(totalUsers, () => {
Expand All @@ -12,6 +13,11 @@ async function createMembers(totalUsers = 1) {
lastName: faker.name.lastName(),
});
});

if (register) {
await Promise.all(this.userNames.map(({ email }) => this.users
.dispatch('register', { params: { username: email, password: '123', audience: '*.localhost' } })));
}
}

exports.createMembers = createMembers;
Expand Down
29 changes: 24 additions & 5 deletions test/suites/organization/create.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/* eslint-disable promise/always-return, no-prototype-builtins */
const { inspectPromise } = require('@makeomatic/deploy');
const assert = require('assert');
const sinon = require('sinon');
const faker = require('faker');
const { createOrganization } = require('../../helpers/organization');
const { createMembers, createOrganization } = require('../../helpers/organization');
const hashPassword = require('../../../src/utils/register/password/hash');
const registerOrganizationMembers = require('../../../src/utils/organization/registerOrganizationMembers');

describe('#create organization', function registerSuite() {
this.timeout(50000);

beforeEach(global.startService);
beforeEach(function () { return createOrganization.call(this, {}, 2); });
beforeEach(function () { return createMembers.call(this, 1); });
afterEach(global.clearRedis);

it('must reject invalid organization params and return detailed error', function test() {
Expand All @@ -21,7 +24,10 @@ describe('#create organization', function registerSuite() {
});
});

it('must be able to create organization', function test() {
it('must be able to create organization and register user', async function test() {
const sendInviteMailSpy = sinon.spy(registerOrganizationMembers, 'call');
const generatePasswordSpy = sinon.spy(hashPassword, 'call');

const params = {
name: faker.company.companyName(),
metadata: {
Expand All @@ -30,20 +36,33 @@ describe('#create organization', function registerSuite() {
members: this.userNames.slice(0, 2),
};

return this.dispatch('users.organization.create', params)
await this.dispatch('users.organization.create', params)
.reflect()
.then(inspectPromise(true))
.then((response) => {
const createdOrganization = response.data.attributes;
assert(createdOrganization.name === params.name);
assert(createdOrganization.metadata.description === params.metadata.description);
assert(createdOrganization.members.length === 2);
assert(createdOrganization.members.length === 1);
assert.ok(createdOrganization.id);
assert(createdOrganization.active === false);
});

const [registeredMember] = await sendInviteMailSpy.returnValues[0];
const registeredMemberPassword = generatePasswordSpy.firstCall.args[1];
const loginParams = {
username: registeredMember.email,
password: registeredMemberPassword,
audience: '*.localhost',
};

return this.users.dispatch('login', { params: loginParams })
.reflect()
.then(inspectPromise());
});

it('must return organization exists error', async function test() {
await createOrganization.call(this, {}, 2);
const params = {
name: this.organization.name,
};
Expand Down
4 changes: 2 additions & 2 deletions test/suites/organization/invites/accept.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('#accept invite organization', function registerSuite() {
this.timeout(50000);

beforeEach(global.startService);
beforeEach(function () { return createMembers.call(this, 2); });
beforeEach(function () { return createOrganization.call(this, {}, 2); });
beforeEach(function () { return createMembers.call(this, 1, true); });
beforeEach(function () { return createOrganization.call(this, {}, 1); });
afterEach(global.clearRedis);

it('must reject invalid organization params and return detailed error', function test() {
Expand Down
2 changes: 1 addition & 1 deletion test/suites/organization/members/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('#edit member permission', function registerSuite() {
it('must be able to edit member permission', async function test() {
const opts = {
organizationId: this.organization.id,
username: this.userNames[0].username,
username: this.userNames[0].email,
permission: {
$set: ['admin'],
},
Expand Down
2 changes: 1 addition & 1 deletion test/suites/organization/members/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('#remove member from organization', function registerSuite() {
it('must be able to remove member', async function test() {
const opts = {
organizationId: this.organization.id,
username: this.userNames[0].username,
username: this.userNames[0].email,
};

return this.dispatch('users.organization.members.remove', opts)
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5253,9 +5253,9 @@ ms-mailer-client@^8.0.1:
lodash.merge "^4.6.1"

ms-mailer-templates@^1.11.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/ms-mailer-templates/-/ms-mailer-templates-1.13.1.tgz#29de47138f68633a3fc596fe99f86ffbe578bb89"
integrity sha512-LIVP1aolT7fefp/6lefDr2Xak2Uah+OLD53LBq75kU9Y7kBNDAIWmAs5Evsp5U84xJAGCC+yz29FkHAUGR+Ybg==
version "1.14.0"
resolved "https://registry.yarnpkg.com/ms-mailer-templates/-/ms-mailer-templates-1.14.0.tgz#582490a194f98f5a68ff38c3795210a34fa2326a"
integrity sha512-Rzto3y3+htJD1gVVbIqgD+fJGjZy9sK/xu23lVU9bs/N/bL8P68M67nXYI6tsgmeHdjnX4mokz+gqADiZFTtYA==
dependencies:
bluebird "^3.5.1"
common-errors "^1.0.5"
Expand Down