0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

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

View file

@ -195,17 +195,16 @@ settingsResult = function (settings, type) {
populateDefaultSetting = function (key) { populateDefaultSetting = function (key) {
// Call populateDefault and update the settings cache // Call populateDefault and update the settings cache
return dataProvider.Settings.populateDefault(key).then(function (defaultSetting) { return dataProvider.Settings.populateDefault(key).then(function (defaultSetting) {
// Process the default result and add to settings cache // Process the default result and add to settings cache
var readResult = readSettingsResult([defaultSetting]); var readResult = readSettingsResult([defaultSetting]);
// Add to the settings cache // Add to the settings cache
return updateSettingsCache(readResult).then(function () { return updateSettingsCache(readResult).then(function () {
// Update theme with the new settings // Try to update theme with the new settings
return config.theme.update(settings, config.url); // 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 () { }).then(function () {
// Get the result from the cache with permission checks // Get the result from the cache with permission checks
return defaultSetting;
}); });
}).otherwise(function (err) { }).otherwise(function (err) {
// Pass along NotFoundError // Pass along NotFoundError

View file

@ -14,7 +14,8 @@ var when = require('when'),
docName = 'users', docName = 'users',
// TODO: implement created_by, updated_by // TODO: implement created_by, updated_by
allowedIncludes = ['permissions', 'roles', 'roles.permissions'], allowedIncludes = ['permissions', 'roles', 'roles.permissions'],
users; users,
sendInviteEmail;
// ## Helpers // ## Helpers
function prepareInclude(include) { function prepareInclude(include) {
@ -22,6 +23,48 @@ function prepareInclude(include) {
return 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 * ## Posts API Methods
* *
@ -43,7 +86,7 @@ users = {
} }
return dataProvider.User.findPage(options); return dataProvider.User.findPage(options);
}).catch(function (error) { }).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 // Check permissions
return canThis(options.context).edit.user(options.id).then(function () { return canThis(options.context).edit.user(options.id).then(function () {
if (data.users[0].roles) { if (data.users[0].roles && data.users[0].roles[0]) {
if (options.id === options.context.user) { var role = data.users[0].roles[0],
return when.reject(new errors.NoPermissionError('You cannot change your own role.')); roleId = parseInt(role.id || role, 10);
}
return canThis(options.context).assign.role(data.users[0].roles[0]).then(function () { 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();
}); });
} }
return editOperation(); return editOperation();
}); });
}).catch(function (error) { }).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 * @returns {Promise(User}} Newly created user
*/ */
add: function add(object, options) { add: function add(object, options) {
var newUser, var addOperation,
user, newUser,
roleId, user;
emailData;
return canThis(options.context).add.user(object).then(function () { if (options.include) {
return utils.checkObject(object, docName).then(function (checkedUserData) { 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)) { addOperation = function () {
return when.reject(new errors.BadRequestError('No role provided.')); if (newUser.email) {
} newUser.name = object.users[0].email.substring(0, newUser.email.indexOf('@'));
newUser.password = globalUtils.uid(50);
roleId = parseInt(newUser.roles[0].id || newUser.roles[0], 10); newUser.status = 'invited';
// 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);
} else { } else {
// only invitations for already invited users are resent return when.reject(new errors.BadRequestError('No email provided.'));
if (foundUser.get('status') === 'invited' || foundUser.get('status') === 'invited-pending') { }
return foundUser;
return dataProvider.User.getByEmail(
newUser.email
).then(function (foundUser) {
if (!foundUser) {
return dataProvider.User.add(newUser, options);
} else { } else {
return when.reject(new errors.BadRequestError('User is already registered.')); // only invitations for already invited users are resent
} if (foundUser.get('status') === 'invited' || foundUser.get('status') === 'invited-pending') {
} return foundUser;
}).then(function (invitedUser) { } else {
user = invitedUser.toJSON(); return when.reject(new errors.BadRequestError('User is already registered.'));
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();
});
} }
}); }
}); }).then(function (invitedUser) {
}).then(function () { user = invitedUser.toJSON();
return when.resolve({users: [user]}); return sendInviteEmail(user);
}).catch(function (error) { }).then(function () {
if (error && error.type === 'EmailError') { // If status was invited-pending and sending the invitation succeeded, set status to invited.
error.message = 'Error sending email: ' + error.message + ' Please check your email settings and resend the invitation.'; if (user.status === 'invited-pending') {
errors.logWarn(error.message); return dataProvider.User.edit(
{status: 'invited'}, _.extend({}, options, {id: user.id})
// If sending the invitation failed, set status to invited-pending ).then(function (editedUser) {
return dataProvider.User.edit({status: 'invited-pending'}, {id: user.id}).then(function (user) { console.log('user to return 2', user);
return dataProvider.User.findOne({ id: user.id }, options).then(function (user) { user = editedUser.toJSON();
return { users: [user] };
}); });
}
}).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) { }).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); return errors.handleAPIError(error);
}); });
}).catch(function (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) { 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)) { if (_.isString(error)) {

View file

@ -397,7 +397,8 @@ User = ghostBookshelf.Model.extend({
options.withRelated = _.union([ 'roles' ], options.include); options.withRelated = _.union([ 'roles' ], options.include);
return Role.findOne({name: 'Author'}).then(function (authorRole) { 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 // 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 // check for too many roles
if (roles.length > 1) { if (roles.length > 1) {
return when.reject(new errors.ValidationError('Only one role per user is supported at the moment.')); 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; userData = addedUser;
//if we are given a "role" object, only pass in the role ID in place of the full object //if we are given a "role" object, only pass in the role ID in place of the full object
roles = _.map(roles, function (role) { roles = _.map(roles, function (role) {
if (_.isNumber(role)) { if (_.isString(role)) {
return parseInt(role, 10);
} else if (_.isNumber(role)) {
return role; return role;
} else { } else {
return parseInt(role.id, 10); 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 // 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 // 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. // 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 // 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 // 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 // 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. // 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) { var userWithEmail = users.find(function (user) {
return user.get('email').toLowerCase() === email.toLowerCase(); return user.get('email').toLowerCase() === email.toLowerCase();
}); });

View file

@ -308,6 +308,26 @@ describe('Users API', function () {
done(); done();
}).catch(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 () { describe('Add', function () {
@ -384,6 +404,30 @@ describe('Users API', function () {
done(); done();
}).catch(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 () { describe('Admin', function () {
@ -485,7 +529,7 @@ describe('Users API', function () {
}); });
}); });
}); });
describe('Destroy', function () { describe('Destroy', function () {
function checkDestroyResponse(response) { function checkDestroyResponse(response) {