User edit & add endpoints cleanup

- edit and add endpoints don't assume role
- edit and add endpoints cope with no role, role objects, and strings
- resend user invite was failing at one point due to no role being sent, but this shouldn't be required
- other random api cleanup
This commit is contained in:
Hannah Wolfe 2014-07-31 01:15:34 +01:00
parent cc995e8ef6
commit eecbdc1693
6 changed files with 237 additions and 165 deletions

View File

@ -46,27 +46,22 @@ authentication = {
return dataProvider.User.generateResetToken(email, expires, dbHash);
}).then(function (resetToken) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/',
emailData = {
resetUrl: resetUrl
};
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/';
return emailData;
}).then(function (emailData) {
return mail.generateContent({data: emailData, template: 'reset-password'}).then(function (emailContent) {
var payload = {
mail: [{
message: {
to: email,
subject: 'Reset Password',
html: emailContent.html,
text: emailContent.text
},
options: {}
}]
};
return mail.send(payload, {context: {internal: true}});
});
return mail.generateContent({data: { resetUrl: resetUrl }, template: 'reset-password'});
}).then(function (emailContent) {
var payload = {
mail: [{
message: {
to: email,
subject: 'Reset Password',
html: emailContent.html,
text: emailContent.text
},
options: {}
}]
};
return mail.send(payload, {context: {internal: true}});
}).then(function () {
return when.resolve({passwordreset: [{message: 'Check your email for further instructions.'}]});
}).otherwise(function (error) {
@ -219,29 +214,27 @@ authentication = {
ownerEmail: setupUser.email
};
return mail.generateContent({data: data, template: 'welcome'}).then(function (emailContent) {
var message = {
to: setupUser.email,
subject: 'Your New Ghost Blog',
html: emailContent.html,
text: emailContent.text
},
payload = {
mail: [{
message: message,
options: {}
}]
};
return mail.generateContent({data: data, template: 'welcome'});
}).then(function (emailContent) {
var message = {
to: setupUser.email,
subject: 'Your New Ghost Blog',
html: emailContent.html,
text: emailContent.text
},
payload = {
mail: [{
message: message,
options: {}
}]
};
return payload;
}).then(function (payload) {
return mail.send(payload, {context: {internal: true}}).otherwise(function (error) {
errors.logError(
error.message,
"Unable to send welcome email, your blog will continue to function.",
"Please see http://docs.ghost.org/mail/ for instructions on configuring email."
);
});
return mail.send(payload, {context: {internal: true}}).otherwise(function (error) {
errors.logError(
error.message,
"Unable to send welcome email, your blog will continue to function.",
"Please see http://docs.ghost.org/mail/ for instructions on configuring email."
);
});
}).then(function () {

View File

@ -195,17 +195,16 @@ settingsResult = function (settings, type) {
populateDefaultSetting = function (key) {
// Call populateDefault and update the settings cache
return dataProvider.Settings.populateDefault(key).then(function (defaultSetting) {
// Process the default result and add to settings cache
var readResult = readSettingsResult([defaultSetting]);
// Add to the settings cache
return updateSettingsCache(readResult).then(function () {
// Update theme with the new settings
return config.theme.update(settings, config.url);
// Try to update theme with the new settings
// if we're in the middle of populating, this might not work
return config.theme.update(settings, config.url).then(function () { return; }, function () { return; });
}).then(function () {
// Get the result from the cache with permission checks
return defaultSetting;
});
}).otherwise(function (err) {
// Pass along NotFoundError

View File

@ -14,7 +14,8 @@ var when = require('when'),
docName = 'users',
// TODO: implement created_by, updated_by
allowedIncludes = ['permissions', 'roles', 'roles.permissions'],
users;
users,
sendInviteEmail;
// ## Helpers
function prepareInclude(include) {
@ -22,6 +23,48 @@ function prepareInclude(include) {
return include;
}
sendInviteEmail = function sendInviteEmail(user) {
var emailData;
return when.join(
users.read({'id': user.created_by}),
settings.read({'key': 'title'}),
settings.read({context: {internal: true}, key: 'dbHash'})
).then(function (values) {
var invitedBy = values[0].users[0],
blogTitle = values[1].settings[0].value,
expires = Date.now() + (14 * globalUtils.ONE_DAY_MS),
dbHash = values[2].settings[0].value;
emailData = {
blogName: blogTitle,
invitedByName: invitedBy.name,
invitedByEmail: invitedBy.email
};
return dataProvider.User.generateResetToken(user.email, expires, dbHash);
}).then(function (resetToken) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url;
emailData.resetLink = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + resetToken + '/';
return mail.generateContent({data: emailData, template: 'invite-user'});
}).then(function (emailContent) {
var payload = {
mail: [{
message: {
to: user.email,
subject: emailData.invitedByName + ' has invited you to join ' + emailData.blogName,
html: emailContent.html,
text: emailContent.text
},
options: {}
}]
};
return mail.send(payload, {context: {internal: true}});
});
};
/**
* ## Posts API Methods
*
@ -43,7 +86,7 @@ users = {
}
return dataProvider.User.findPage(options);
}).catch(function (error) {
return errors.handleAPIError(error);
return errors.handleAPIError(error, 'You do not have permission to browse users.');
});
},
@ -106,20 +149,32 @@ users = {
// Check permissions
return canThis(options.context).edit.user(options.id).then(function () {
if (data.users[0].roles) {
if (options.id === options.context.user) {
return when.reject(new errors.NoPermissionError('You cannot change your own role.'));
}
return canThis(options.context).assign.role(data.users[0].roles[0]).then(function () {
if (data.users[0].roles && data.users[0].roles[0]) {
var role = data.users[0].roles[0],
roleId = parseInt(role.id || role, 10);
return dataProvider.User.findOne(
{id: options.context.user, include: 'roles'}
).then(function (contextUser) {
var contextRoleId = contextUser.related('roles').toJSON()[0].id;
if (roleId !== contextRoleId &&
parseInt(options.id, 10) === parseInt(options.context.user, 10)) {
return when.reject(new errors.NoPermissionError('You cannot change your own role.'));
} else if (roleId !== contextRoleId) {
return canThis(options.context).assign.role(role).then(function () {
return editOperation();
});
}
return editOperation();
});
}
return editOperation();
});
}).catch(function (error) {
return errors.handleAPIError(error);
return errors.handleAPIError(error, 'You do not have permission to edit this user');
});
},
@ -130,119 +185,92 @@ users = {
* @returns {Promise(User}} Newly created user
*/
add: function add(object, options) {
var newUser,
user,
roleId,
emailData;
var addOperation,
newUser,
user;
return canThis(options.context).add.user(object).then(function () {
return utils.checkObject(object, docName).then(function (checkedUserData) {
if (options.include) {
options.include = prepareInclude(options.include);
}
if (options.include) {
options.include = prepareInclude(options.include);
}
newUser = checkedUserData.users[0];
return utils.checkObject(object, docName).then(function (data) {
newUser = data.users[0];
if (_.isEmpty(newUser.roles)) {
return when.reject(new errors.BadRequestError('No role provided.'));
}
roleId = parseInt(newUser.roles[0].id || newUser.roles[0], 10);
// 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 when.reject(new errors.NoPermissionError('Not allowed to create an owner user.'));
}
return canThis(options.context).assign.role(role);
}).then(function () {
if (newUser.email) {
newUser.name = object.users[0].email.substring(0, newUser.email.indexOf('@'));
newUser.password = globalUtils.uid(50);
newUser.status = 'invited';
} else {
return when.reject(new errors.BadRequestError('No email provided.'));
}
}).catch(function () {
return when.reject(new errors.NoPermissionError('Not allowed to create user with that role.'));
});
}).then(function () {
return dataProvider.User.getByEmail(newUser.email);
}).then(function (foundUser) {
if (!foundUser) {
return dataProvider.User.add(newUser, options);
addOperation = function () {
if (newUser.email) {
newUser.name = object.users[0].email.substring(0, newUser.email.indexOf('@'));
newUser.password = globalUtils.uid(50);
newUser.status = 'invited';
} else {
// only invitations for already invited users are resent
if (foundUser.get('status') === 'invited' || foundUser.get('status') === 'invited-pending') {
return foundUser;
return when.reject(new errors.BadRequestError('No email provided.'));
}
return dataProvider.User.getByEmail(
newUser.email
).then(function (foundUser) {
if (!foundUser) {
return dataProvider.User.add(newUser, options);
} else {
return when.reject(new errors.BadRequestError('User is already registered.'));
}
}
}).then(function (invitedUser) {
user = invitedUser.toJSON();
return settings.read({context: {internal: true}, key: 'dbHash'});
}).then(function (response) {
var expires = Date.now() + (14 * globalUtils.ONE_DAY_MS),
dbHash = response.settings[0].value;
return dataProvider.User.generateResetToken(user.email, expires, dbHash);
}).then(function (resetToken) {
return when.join(users.read({'id': user.created_by}), settings.read({'key': 'title'})).then(function (values) {
var invitedBy = values[0].users[0],
blogTitle = values[1].settings[0].value,
baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + resetToken + '/';
emailData = {
blogName: blogTitle,
invitedByName: invitedBy.name,
invitedByEmail: invitedBy.email,
resetLink: resetUrl
};
return mail.generateContent({data: emailData, template: 'invite-user'});
}).then(function (emailContent) {
var payload = {
mail: [
{
message: {
to: user.email,
subject: emailData.invitedByName + ' has invited you to join ' + emailData.blogName,
html: emailContent.html,
text: emailContent.text
},
options: {}
}
]
};
return mail.send(payload, {context: {internal: true}}).then(function () {
// If status was invited-pending and sending the invitation succeeded, set status to invited.
if (user.status === 'invited-pending') {
return dataProvider.User.edit({status: 'invited'}, {id: user.id}).then(function (editedUser) {
user = editedUser.toJSON();
});
// only invitations for already invited users are resent
if (foundUser.get('status') === 'invited' || foundUser.get('status') === 'invited-pending') {
return foundUser;
} else {
return when.reject(new errors.BadRequestError('User is already registered.'));
}
});
});
}).then(function () {
return when.resolve({users: [user]});
}).catch(function (error) {
if (error && error.type === 'EmailError') {
error.message = 'Error sending email: ' + error.message + ' Please check your email settings and resend the invitation.';
errors.logWarn(error.message);
// If sending the invitation failed, set status to invited-pending
return dataProvider.User.edit({status: 'invited-pending'}, {id: user.id}).then(function (user) {
return dataProvider.User.findOne({ id: user.id }, options).then(function (user) {
return { users: [user] };
}
}).then(function (invitedUser) {
user = invitedUser.toJSON();
return sendInviteEmail(user);
}).then(function () {
// If status was invited-pending and sending the invitation succeeded, set status to invited.
if (user.status === 'invited-pending') {
return dataProvider.User.edit(
{status: 'invited'}, _.extend({}, options, {id: user.id})
).then(function (editedUser) {
console.log('user to return 2', user);
user = editedUser.toJSON();
});
}
}).then(function () {
return when.resolve({users: [user]});
}).catch(function (error) {
if (error && error.type === 'EmailError') {
error.message = 'Error sending email: ' + error.message + ' Please check your email settings and resend the invitation.';
errors.logWarn(error.message);
// If sending the invitation failed, set status to invited-pending
return dataProvider.User.edit({status: 'invited-pending'}, {id: user.id}).then(function (user) {
return dataProvider.User.findOne({ id: user.id }, options).then(function (user) {
return { users: [user] };
});
});
}
return when.reject(error);
});
};
// Check permissions
return canThis(options.context).add.user(object).then(function () {
if (newUser.roles && newUser.roles[0]) {
var roleId = parseInt(newUser.roles[0].id || newUser.roles[0], 10);
// 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 when.reject(new errors.NoPermissionError('Not allowed to create an owner user.'));
}
return canThis(options.context).assign.role(role);
}).then(function () {
return addOperation();
});
}
return when.reject(error);
return addOperation();
});
}).catch(function (error) {
return errors.handleAPIError(error);
return errors.handleAPIError(error, 'You do not have permission to add this user');
});
},
@ -273,7 +301,7 @@ users = {
return errors.handleAPIError(error);
});
}).catch(function (error) {
return errors.handleAPIError(error);
return errors.handleAPIError(error, 'You do not have permission to destroy this user');
});
},

View File

@ -154,9 +154,11 @@ errors = {
};
},
handleAPIError: function (error) {
handleAPIError: function (error, permsMessage) {
if (!error) {
return this.rejectError(new this.NoPermissionError('You do not have permission to perform this action'));
return this.rejectError(
new this.NoPermissionError(permsMessage || 'You do not have permission to perform this action')
);
}
if (_.isString(error)) {

View File

@ -397,7 +397,8 @@ User = ghostBookshelf.Model.extend({
options.withRelated = _.union([ 'roles' ], options.include);
return Role.findOne({name: 'Author'}).then(function (authorRole) {
// Get the role we're going to assign to this user, or the author role if there isn't one
roles = data.roles || authorRole.id;
roles = data.roles || [authorRole.get('id')];
// check for too many roles
if (roles.length > 1) {
return when.reject(new errors.ValidationError('Only one role per user is supported at the moment.'));
@ -424,7 +425,9 @@ User = ghostBookshelf.Model.extend({
userData = addedUser;
//if we are given a "role" object, only pass in the role ID in place of the full object
roles = _.map(roles, function (role) {
if (_.isNumber(role)) {
if (_.isString(role)) {
return parseInt(role, 10);
} else if (_.isNumber(role)) {
return role;
} else {
return parseInt(role.id, 10);
@ -781,12 +784,15 @@ User = ghostBookshelf.Model.extend({
// Get the user by email address, enforces case insensitivity rejects if the user is not found
// When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
// joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
getByEmail: function (email) {
getByEmail: function (email, options) {
options = options || {};
// We fetch all users and process them in JS as there is no easy way to make this query across all DBs
// Although they all support `lower()`, sqlite can't case transform unicode characters
// This is somewhat mute, as validator.isEmail() also doesn't support unicode, but this is much easier / more
// likely to be fixed in the near future.
return Users.forge().fetch({require: true}).then(function (users) {
options.require = true;
return Users.forge(options).fetch(options).then(function (users) {
var userWithEmail = users.find(function (user) {
return user.get('email').toLowerCase() === email.toLowerCase();
});

View File

@ -308,6 +308,26 @@ describe('Users API', function () {
done();
}).catch(done);
});
it('Author can edit self with role set', function (done) {
// Next test that author CAN edit self
UserAPI.edit(
{users: [{name: newName, roles: [roleIdFor.author]}]}, _.extend({}, context.author, {id: userIdFor.author})
).then(function (response) {
checkEditResponse(response);
done();
}).catch(done);
});
it('Author can edit self with role set as string', function (done) {
// Next test that author CAN edit self
UserAPI.edit(
{users: [{name: newName, roles: [roleIdFor.author.toString()]}]}, _.extend({}, context.author, {id: userIdFor.author})
).then(function (response) {
checkEditResponse(response);
done();
}).catch(done);
});
});
describe('Add', function () {
@ -384,6 +404,30 @@ describe('Users API', function () {
done();
}).catch(done);
});
it('Can add with no role set', function (done) {
// Can add author
delete newUser.roles;
UserAPI.add({users: [newUser]}, _.extend({}, context.owner, {include: 'roles'}))
.then(function (response) {
checkAddResponse(response);
response.users[0].id.should.eql(8);
response.users[0].roles[0].name.should.equal('Author');
done();
}).catch(done);
});
it('Can add with role set as string', function (done) {
// Can add author
newUser.roles = [roleIdFor.author.toString()];
UserAPI.add({users: [newUser]}, _.extend({}, context.owner, {include: 'roles'}))
.then(function (response) {
checkAddResponse(response);
response.users[0].id.should.eql(8);
response.users[0].roles[0].name.should.equal('Author');
done();
}).catch(done);
});
});
describe('Admin', function () {
@ -485,7 +529,7 @@ describe('Users API', function () {
});
});
});
describe('Destroy', function () {
function checkDestroyResponse(response) {