Skip to content

Commit

Permalink
🎨 Separate invites from user
Browse files Browse the repository at this point in the history
refs TryGhost#7420
- remove invite logic from user
- add invite model and adapt affected logic for inviting team members
  • Loading branch information
kirrg001 authored and geekhuyang committed Nov 20, 2016
1 parent 4fd5794 commit 32a56f2
Show file tree
Hide file tree
Showing 22 changed files with 1,009 additions and 496 deletions.
84 changes: 54 additions & 30 deletions core/server/api/authentication.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
var _ = require('lodash'),
validator = require('validator'),
Promise = require('bluebird'),
pipeline = require('../utils/pipeline'),
dataProvider = require('../models'),
settings = require('./settings'),
mail = require('./../mail'),
apiMail = require('./mail'),
globalUtils = require('../utils'),
utils = require('./utils'),
errors = require('../errors'),
models = require('../models'),
events = require('../events'),
config = require('../config'),
i18n = require('../i18n'),
Expand Down Expand Up @@ -72,7 +73,7 @@ function setupTasks(setupData) {

function setupUser(userData) {
var context = {context: {internal: true}},
User = dataProvider.User;
User = models.User;

return User.findOne({role: 'Owner', status: 'all'}).then(function then(owner) {
if (!owner) {
Expand Down Expand Up @@ -158,7 +159,7 @@ authentication = {
var dbHash = response.settings[0].value,
expiresAt = Date.now() + globalUtils.ONE_DAY_MS;

return dataProvider.User.generateResetToken(email, expiresAt, dbHash);
return models.User.generateResetToken(email, expiresAt, dbHash);
}).then(function then(resetToken) {
return {
email: email,
Expand Down Expand Up @@ -235,7 +236,7 @@ authentication = {
ne2Password = data.ne2Password;

return settings.read(settingsQuery).then(function then(response) {
return dataProvider.User.resetPassword({
return models.User.resetPassword({
token: resetToken,
newPassword: newPassword,
ne2Password: ne2Password,
Expand Down Expand Up @@ -270,33 +271,56 @@ authentication = {
* @returns {Promise<Object>}
*/
acceptInvitation: function acceptInvitation(invitation) {
var tasks;
var tasks, invite, options = {context: {internal: true}};

function validateInvitation(invitation) {
return utils.checkObject(invitation, 'invitation');
return utils.checkObject(invitation, 'invitation')
.then(function () {
if (!invitation.invitation[0].token) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noTokenProvided')));
}

if (!invitation.invitation[0].email) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noEmailProvided')));
}

if (!invitation.invitation[0].password) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noPasswordProvided')));
}

if (!invitation.invitation[0].name) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.authentication.noNameProvided')));
}

return invitation;
});
}

function processInvitation(invitation) {
var User = dataProvider.User,
settingsQuery = {context: {internal: true}, key: 'dbHash'},
data = invitation.invitation[0],
resetToken = data.token,
newPassword = data.password,
email = data.email,
name = data.name;
var data = invitation.invitation[0], inviteToken = globalUtils.decodeBase64URLsafe(data.token);

return settings.read(settingsQuery).then(function then(response) {
return User.resetPassword({
token: resetToken,
newPassword: newPassword,
ne2Password: newPassword,
dbHash: response.settings[0].value
return models.Invite.findOne({token: inviteToken, status: 'sent'}, _.merge({}, {include: ['roles']}, options))
.then(function (_invite) {
invite = _invite;

if (!invite) {
throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound'));
}

if (invite.get('expires') < Date.now()) {
throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteExpired'));
}

return models.User.add({
email: data.email,
name: data.name,
password: data.password,
roles: invite.toJSON().roles
}, options);
})
.then(function () {
return invite.destroy(options);
});
}).then(function then(user) {
return User.edit({name: name, email: email, slug: ''}, {id: user.id});
}).catch(function (error) {
throw new errors.UnauthorizedError(error.message);
});
}

function formatResponse() {
Expand Down Expand Up @@ -339,8 +363,8 @@ authentication = {
}

function checkInvitation(email) {
return dataProvider.User
.where({email: email, status: 'invited'})
return models.Invite
.where({email: email, status: 'sent'})
.count('id')
.then(function then(count) {
return !!count;
Expand Down Expand Up @@ -370,7 +394,7 @@ authentication = {
validStatuses = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'];

function checkSetupStatus() {
return dataProvider.User
return models.User
.where('status', 'in', validStatuses)
.count('id')
.then(function (count) {
Expand Down Expand Up @@ -478,7 +502,7 @@ authentication = {
}

function checkPermission(options) {
return dataProvider.User.findOne({role: 'Owner', status: 'all'})
return models.User.findOne({role: 'Owner', status: 'all'})
.then(function (owner) {
if (owner.id !== options.context.user) {
throw new errors.NoPermissionError(i18n.t('errors.api.authentication.notTheBlogOwner'));
Expand Down Expand Up @@ -519,8 +543,8 @@ authentication = {

function revokeToken(options) {
var providers = [
dataProvider.Refreshtoken,
dataProvider.Accesstoken
models.Refreshtoken,
models.Accesstoken
],
response = {token: options.token};

Expand Down
4 changes: 3 additions & 1 deletion core/server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var _ = require('lodash'),
roles = require('./roles'),
settings = require('./settings'),
tags = require('./tags'),
invites = require('./invites'),
clients = require('./clients'),
users = require('./users'),
slugs = require('./slugs'),
Expand Down Expand Up @@ -291,7 +292,8 @@ module.exports = {
authentication: authentication,
uploads: uploads,
slack: slack,
themes: themes
themes: themes,
invites: invites
};

/**
Expand Down
208 changes: 208 additions & 0 deletions core/server/api/invites.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
var _ = require('lodash'),
Promise = require('bluebird'),
pipeline = require('../utils/pipeline'),
dataProvider = require('../models'),
settings = require('./settings'),
mail = require('./../mail'),
apiMail = require('./mail'),
globalUtils = require('../utils'),
utils = require('./utils'),
errors = require('../errors'),
config = require('../config'),
i18n = require('../i18n'),
docName = 'invites',
allowedIncludes = ['created_by', 'updated_by', 'roles'],
invites;

invites = {
browse: function browse(options) {
var tasks;

function modelQuery(options) {
return dataProvider.Invite.findPage(options);
}

tasks = [
utils.validate(docName, {opts: utils.browseDefaultOptions}),
utils.handlePublicPermissions(docName, 'browse'),
utils.convertOptions(allowedIncludes),
modelQuery
];

return pipeline(tasks, options);
},

read: function read(options) {
var attrs = ['id', 'email'],
tasks;

function modelQuery(options) {
return dataProvider.Invite.findOne(options.data, _.omit(options, ['data']));
}

tasks = [
utils.validate(docName, {attrs: attrs}),
utils.handlePublicPermissions(docName, 'read'),
utils.convertOptions(allowedIncludes),
modelQuery
];

return pipeline(tasks, options)
.then(function formatResponse(result) {
if (result) {
return {invites: [result.toJSON(options)]};
}

return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound')));
});
},

destroy: function destroy(options) {
var tasks;

function modelQuery(options) {
return dataProvider.Invite.findOne({id: options.id}, _.omit(options, ['data']))
.then(function (invite) {
if (!invite) {
throw new errors.NotFoundError(i18n.t('errors.api.invites.inviteNotFound'));
}

return invite.destroy(options).return(null);
});
}

tasks = [
utils.validate(docName, {opts: utils.idDefaultOptions}),
utils.handlePermissions(docName, 'destroy'),
utils.convertOptions(allowedIncludes),
modelQuery
];

return pipeline(tasks, options);
},

add: function add(object, options) {
var tasks,
loggedInUser = options.context.user,
emailData,
invite;

function addInvite(options) {
var data = options.data;

return dataProvider.User.findOne({id: loggedInUser}, options)
.then(function (user) {
if (!user) {
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.users.userNotFound')));
}

loggedInUser = user;
return dataProvider.Invite.add(data.invites[0], _.omit(options, 'data'));
})
.then(function (_invite) {
invite = _invite;

return settings.read({key: 'title'});
})
.then(function (response) {
var baseUrl = config.get('forceAdminSSL') ? (config.get('urlSSL') || config.get('url')) : config.get('url');

emailData = {
blogName: response.settings[0].value,
invitedByName: loggedInUser.get('name'),
invitedByEmail: loggedInUser.get('email'),
// @TODO: resetLink sounds weird
resetLink: baseUrl.replace(/\/$/, '') + '/ghost/signup/' + globalUtils.encodeBase64URLsafe(invite.get('token')) + '/'
};

return mail.utils.generateContent({data: emailData, template: 'invite-user'});
}).then(function (emailContent) {
var payload = {
mail: [{
message: {
to: invite.get('email'),
subject: i18n.t('common.api.users.mail.invitedByName', {
invitedByName: emailData.invitedByName,
blogName: emailData.blogName
}),
html: emailContent.html,
text: emailContent.text
},
options: {}
}]
};

return apiMail.send(payload, {context: {internal: true}});
}).then(function () {
options.id = invite.id;
return dataProvider.Invite.edit({status: 'sent'}, options);
}).then(function () {
invite.set('status', 'sent');
var inviteAsJSON = invite.toJSON();
return {invites: [inviteAsJSON]};
}).catch(function (error) {
if (error && error.errorType === 'EmailError') {
error.message = i18n.t('errors.api.invites.errorSendingEmail.error', {message: error.message}) + ' ' +
i18n.t('errors.api.invites.errorSendingEmail.help');

errors.logWarn(error.message);
}

return Promise.reject(error);
});
}

function destroyOldInvite(options) {
var data = options.data;

return dataProvider.Invite.findOne({email: data.invites[0].email}, _.omit(options, 'data'))
.then(function (invite) {
if (!invite) {
return Promise.resolve(options);
}

return invite.destroy(options);
})
.then(function () {
return options;
});
}

function validation(options) {
var roleId;

if (!options.data.invites[0].email) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.invites.emailIsRequired')));
}

if (!options.data.invites[0].roles || !options.data.invites[0].roles[0]) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.invites.roleIsRequired')));
}

roleId = parseInt(options.data.invites[0].roles[0].id || options.data.invites[0].roles[0], 10);

// @TODO move this logic to permissible
// Make sure user is allowed to add a user with this role
return dataProvider.Role.findOne({id: roleId}).then(function (role) {
if (role.get('name') === 'Owner') {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.api.invites.notAllowedToInviteOwner')));
}
}).then(function () {
return options;
});
}

tasks = [
utils.validate(docName, {opts: ['email']}),
utils.handlePermissions(docName, 'add'),
utils.convertOptions(allowedIncludes),
validation,
destroyOldInvite,
addInvite
];

return pipeline(tasks, object, options);
}
};

module.exports = invites;
Loading

0 comments on commit 32a56f2

Please sign in to comment.