Skip to content

Bugfix/ask email confirmation when adding organisation manager #3398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
7 changes: 5 additions & 2 deletions apinf_packages/core/lib/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@
"organizationManagersList_title": "Managers",
"organizationManagerForm_successMessage": "New manager added.",
"organizationManagerForm_userNotRegistered_errorText": "User not currently registered.",
"organizationManagerForm_managerAlreadyExist_errorText": "User is already a manager of this organization.",
"organizationNoApis_text_noConnectedApis": "The organization doesn't have any connected APIs.",
"organizationNoApis_text_useButton": "You can connect one via button \"Connect API to Organization\"",
"organizationNoFeaturedApis_text_noFeaturedApis": "The organization doesn't have any featured APIs.",
Expand Down Expand Up @@ -534,7 +533,10 @@
"organizationSettings_uploadLogoText": "You can upload a logo for your organization. The logo will appear in the catalog and on the single organization profile.",
"organizationSettings_uploadCoverText": "You can upload a cover image for your organization.",
"organizationSettings_title_organizationManagers": "Organization Managers",
"organizationSettings_description_organizationManagers": "An organization manager has the same rights as the original API manager to edit, delete the API after an API has been connected to an organization.",
"organizationSettings_description_organizationManagers": "An organization manager has the same rights as those of the original API manager to edit and delete the APIs which are connected to this organization.",
"organizationManagerForm_managerAlreadyExist_errorText": "User is already a manager of this organization.",
"organizationManagerForm_sendingVerificationEmailFailed": "Sending verification email failed",
"organizationManagerForm_invalidMailSetting_emailFailed": "Sending verification email failed:Invalid mail settings",
"organizationSettingsDelete_buttonText_delete": "Delete",
"organizationSettingsDelete_text_information": "The organization information will be removed and all APIs will be disconnected. This action cannot be undone.",
"organizationSettingsDelete_title": "Delete Organization",
Expand All @@ -555,6 +557,7 @@
"organizationSettingsSocialMedia_title": "Social Media Platforms",
"organizationSettingsSocialMedia_update_successMessage": "Social media information has updated",
"organizationSettingsSocialMedia_update_failedMessage": "Social media information updation failed",
"organizationSettings_listManager_emailUnverifiedLabel": "Unverified",
"privacyPolicy_title": "Privacy Policy",
"profile_Header": "Profile",
"profile_UpdateButton": "Update",
Expand Down
6 changes: 6 additions & 0 deletions apinf_packages/organizations/client/add/autoform.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ AutoForm.hooks({

// Add current user as Organization manager & creater
organization.managerIds = [userId];
// Set default value for the owner of organization
organization.emailVerification = [{
managerIds: userId,
verified: true,
}];

organization.createdBy = userId;

// Submit the form
Expand Down
23 changes: 23 additions & 0 deletions apinf_packages/organizations/client/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Meteor } from 'meteor/meteor';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { DocHead } from 'meteor/kadira:dochead';
import { TAPi18n } from 'meteor/tap:i18n';
import { sAlert } from 'meteor/juliancwirko:s-alert';

FlowRouter.route('/organizations', {
// Get query parameters for Catalog page on Enter
Expand Down Expand Up @@ -74,3 +76,24 @@ FlowRouter.route('/organizations/:orgSlug/', {
});
},
});

FlowRouter.route('/email-verify/:token/:slug', {
name: 'email-verification',
action (params) {
// Get token from Router params
const token = params.token;
const slug = params.slug;
// Update verification status
Meteor.call('verifyToken', token, (error) => {
if (error) {
// Show token invalid
sAlert.error(error.error, { timeout: 'none' });
} else {
// Email successfully verified
sAlert.success(TAPi18n.__('emailVerification_successMessage'));
}
});
// Go to front page
FlowRouter.go('organizationProfile', { orgSlug: slug });
},
});
16 changes: 16 additions & 0 deletions apinf_packages/organizations/client/managers/form/autoform.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ AutoForm.hooks({
// Display error
sAlert.error(message, { timeout: 'none' });
}

if (errorType === 'email-failed') {
// Get error message translation
const message = TAPi18n.__('organizationManagerForm_sendingVerificationEmailFailed');

// Show error to manager that verification email failed
sAlert.error(message, { timeout: 'none' });
}

if (errorType === 'email-failed-mail-setting-invalid') {
// Get error message translation
const message = TAPi18n.__('organizationManagerForm_invalidMailSetting_emailFailed');

// Show error to manager that mail settings invalid
sAlert.error(message, { timeout: 'none' });
}
},
},
});
22 changes: 14 additions & 8 deletions apinf_packages/organizations/client/managers/list/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@
<li class="list-group-item">
{{ username }}
({{ email }})

{{# if hasMultipleManagers }}
<button
id="remove-organization-manager"
class="btn btn-danger btn-xs pull-right">
<i class="fa fa-trash-o fa-lg" aria-hidden="true"></i>
</button>
{{/ if }}
<div class="pull-right">
{{# unless emailVerified }}
<span class="label label-default">
{{_ 'organizationSettings_listManager_emailUnverifiedLabel' }}
</span>&nbsp;
{{/ unless }}
{{# if hasMultipleManagers }}
<button
id="remove-organization-manager"
class="btn btn-danger btn-xs">
<i class="fa fa-trash-o fa-lg" aria-hidden="true"></i>
</button>
{{/ if }}
</div>
</li>
{{/ each }}
</ul>
Expand Down
10 changes: 10 additions & 0 deletions apinf_packages/organizations/client/managers/list/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,20 @@ Template.organizationManagersList.helpers({

// flatten structure of users within organization managers array
organizationManagers = _.map(organizationManagers, (user) => {
// Construct empty object
let verificationData = {};
const result = _.find(organization.emailVerification, { managerIds: user._id });
if (result) {
verificationData = result;
} else {
verificationData.verified = organization.createdBy === user._id;
}

return {
username: user.username,
email: user.emails[0].address,
_id: user._id,
emailVerified: verificationData.verified,
};
});

Expand Down
19 changes: 19 additions & 0 deletions apinf_packages/organizations/collection/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ Organizations.schema = new SimpleSchema({
label: false,
},
},
emailVerification: {
type: [Object],
optional: true,
},
'emailVerification.$.managerIds': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoform: {
type: 'hidden',
label: false,
},
},
'emailVerification.$.verified': {
type: Boolean,
},
'emailVerification.$.verificationToken': {
type: String,
optional: true,
},
name: {
type: String,
optional: false,
Expand Down
144 changes: 138 additions & 6 deletions apinf_packages/organizations/server/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import slugs from 'limax';
// Meteor packages imports
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { Random } from 'meteor/random';
import { Email } from 'meteor/email';

// Meteor contributed packages imports
import { Accounts } from 'meteor/accounts-base';
Expand All @@ -21,6 +23,12 @@ import _ from 'lodash';
import Apis from '/apinf_packages/apis/collection';
import OrganizationApis from '/apinf_packages/organization_apis/collection';
import Organizations from '/apinf_packages/organizations/collection';
import Settings from '/apinf_packages/settings/collection';

// APInf imports
import {
mailSettingsValid,
} from '/apinf_packages/core/helper_functions/validate_settings';

Meteor.methods({
getCurrentUserUnlinkedApis () {
Expand Down Expand Up @@ -78,10 +86,45 @@ Meteor.methods({

// Check if user is already a manager
const alreadyManager = organization.managerIds.includes(user._id);

// Check if the user is already a manager
if (alreadyManager) {
throw new Meteor.Error('manager-already-exist');
} else {
// Construct object and assign manager ID
const emailVerification = {
managerIds: user._id,
};
// Ignore organization creator
if (organization.createdBy === user._id) {
// Assign default value for creator of organization
emailVerification.verified = true;
emailVerification.verificationToken = '';
} else {
// Send email verification code
const response = Meteor.call('sendEmailVerification', user._id, organization.slug);

// Check error status
if (response.status === 'failed') {
// Check error type
if (response.message === 'email-failed') {
throw new Meteor.Error('email-failed');
} else if (response.message === 'mail-setting-invalid') {
throw new Meteor.Error('email-failed-mail-setting-invalid');
}
}

// Assign response token and verified value
emailVerification.verified = false;
emailVerification.verificationToken = response.token;
}

// Add user ID to manager IDs field
const result = Organizations.update(manager.organizationId,
{ $push: { managerIds: user._id, emailVerification } });

if (!result) {
throw new Meteor.Error('add-manager-fail');
}
}

// Add user ID to manager IDs field
Expand All @@ -95,11 +138,14 @@ Meteor.methods({
check(userId, String);

// Remove User ID from managers array
const result = Organizations.update({ _id: organizationId },
{ $pull:
{ managerIds: userId },
}
);
const result = Organizations.update({ _id: organizationId }, {
$pull: {
managerIds: userId,
emailVerification: {
managerIds: userId,
},
},
});

return result;
},
Expand Down Expand Up @@ -248,4 +294,90 @@ Meteor.methods({
// Return the API slug
return newSlug;
},
sendEmailVerification (managerId, slug) {
// Make sure managerId is a String
check(managerId, String);
// Make sure slug is a String
check(slug, String);

// Get Settings collection
const settings = Settings.findOne();
let token = '';
// Check if mail settings are provided
if (mailSettingsValid(settings)) {
const username = encodeURIComponent(settings.mail.username);
const password = encodeURIComponent(settings.mail.password);
const smtpHost = encodeURIComponent(settings.mail.smtpHost);
const smtpPort = encodeURIComponent(settings.mail.smtpPort);

// Set MAIL_URL env variable
// Note, this must be on one, long line for the URL to be valid
process.env.MAIL_URL = `smtp://${username}:${password}@${smtpHost}:${smtpPort}`;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// Get User collection
const user = Meteor.users.findOne(managerId);
const sendEmailTo = user.emails[0].address;

// Create token
token = Random.secret();

// Get hostname
const hostname = Meteor.absoluteUrl();
const url = `${hostname}email-verify/${token}/${slug}`;

const message = `<p>To verify your email address visit the following link:</p>\n
<p><a href=${url}>${url}</a></p>`;

// try catch here
try {
// Send the e-mail
Email.send({
to: sendEmailTo,
from: settings.mail.fromEmail,
subject: 'Verify Your Email Address',
html: message,
});
} catch (e) {
return {
status: 'failed',
message: 'email-failed',
};
}
} else {
return {
status: 'failed',
message: 'mail-setting-invalid',
};
}

return {
token,
status: 'success',
message: 'email-send-successfully',
};
},
verifyToken (verificationToken) {
// Make sure verificationToken is a String
check(verificationToken, String);

// Get organization document
const organization = Organizations.findOne(
{ 'emailVerification.verificationToken': verificationToken });

// Check organization
if (!organization) {
// Throw token error for client
throw new Meteor.Error('Verification failed. Authentication token does not exist in db.');
}

const resp = Organizations.update(
{ 'emailVerification.verificationToken': verificationToken },
{ $set: {
'emailVerification.$.verified': true,
},
});

return resp;
},
});