mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Merge pull request #3475 from ErisDS/api-clean
User edit & add endpoints cleanup
This commit is contained in:
commit
87b07eb9e0
6 changed files with 237 additions and 165 deletions
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue