Invite user API
closes #3080 - added users.invite() to add user from email with random password - added `GET /ghost/api/v0.1/users/` to invite users and resend invitations - removed one user limit - added global utils for uid generation - changed some „“ to ‚‘
This commit is contained in:
parent
c5771e73bb
commit
79a80b67ac
@ -1,4 +1,3 @@
|
||||
|
||||
var dataProvider = require('../models'),
|
||||
settings = require('./settings'),
|
||||
mail = require('./mail'),
|
||||
@ -6,7 +5,7 @@ var dataProvider = require('../models'),
|
||||
when = require('when'),
|
||||
errors = require('../errors'),
|
||||
config = require('../config'),
|
||||
ONE_DAY = 86400000,
|
||||
ONE_DAY = 60 * 60 * 24 * 1000,
|
||||
authentication;
|
||||
|
||||
/**
|
||||
@ -25,6 +24,7 @@ authentication = {
|
||||
generateResetToken: function generateResetToken(object) {
|
||||
var expires = Date.now() + ONE_DAY,
|
||||
email;
|
||||
|
||||
return utils.checkObject(object, 'passwordreset').then(function (checkedPasswordReset) {
|
||||
if (checkedPasswordReset.passwordreset[0].email) {
|
||||
email = checkedPasswordReset.passwordreset[0].email;
|
||||
@ -34,37 +34,35 @@ authentication = {
|
||||
|
||||
return settings.read({context: {internal: true}, key: 'dbHash'}).then(function (response) {
|
||||
var dbHash = response.settings[0].value;
|
||||
return dataProvider.User.generateResetToken(email, expires, dbHash);
|
||||
}).then(function (resetToken) {
|
||||
var baseUrl = config().forceAdminSSL ? (config().urlSSL || config().url) : config().url,
|
||||
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
|
||||
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/',
|
||||
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
|
||||
payload = {
|
||||
mail: [{
|
||||
message: {
|
||||
to: email,
|
||||
subject: 'Reset Password',
|
||||
html: '<p><strong>Hello!</strong></p>' +
|
||||
'<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
|
||||
'<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
|
||||
'<p>Ghost</p>'
|
||||
},
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
return dataProvider.User.generateResetToken(email, expires, dbHash).then(function (resetToken) {
|
||||
var baseUrl = config().forceAdminSSL ? (config().urlSSL || config().url) : config().url,
|
||||
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
|
||||
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/',
|
||||
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
|
||||
payload = {
|
||||
mail: [{
|
||||
message: {
|
||||
to: email,
|
||||
subject: 'Reset Password',
|
||||
html: '<p><strong>Hello!</strong></p>' +
|
||||
'<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
|
||||
'<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
|
||||
'<p>Ghost</p>'
|
||||
},
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
return mail.send(payload);
|
||||
}).then(function () {
|
||||
return when.resolve({passwordreset: [{message: 'Check your email for further instructions.'}]});
|
||||
}).otherwise(function (error) {
|
||||
// TODO: This is kind of sketchy, depends on magic string error.message from Bookshelf.
|
||||
if (error && error.message === 'NotFound') {
|
||||
error.message = "Invalid email address";
|
||||
}
|
||||
|
||||
return when.reject(new errors.UnauthorizedError(error.message));
|
||||
});
|
||||
return mail.send(payload);
|
||||
}).then(function () {
|
||||
return when.resolve({passwordreset: [{message: 'Check your email for further instructions.'}]});
|
||||
}).otherwise(function (error) {
|
||||
// TODO: This is kind of sketchy, depends on magic string error.message from Bookshelf.
|
||||
if (error && error.message === 'NotFound') {
|
||||
error = new errors.UnauthorizedError('Invalid email address');
|
||||
}
|
||||
return when.reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -7,9 +7,12 @@ var when = require('when'),
|
||||
canThis = require('../permissions').canThis,
|
||||
errors = require('../errors'),
|
||||
utils = require('./utils'),
|
||||
globalUtils = require('../utils'),
|
||||
config = require('../config'),
|
||||
mail = require('./mail'),
|
||||
|
||||
docName = 'users',
|
||||
ONE_DAY = 86400000,
|
||||
ONE_DAY = 60 * 60 * 24 * 1000,
|
||||
users;
|
||||
|
||||
|
||||
@ -130,6 +133,80 @@ users = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Invite user
|
||||
* @param {User} object the user to create
|
||||
* @returns {Promise(User}} Newly created user
|
||||
*/
|
||||
invite: function invite(object, options) {
|
||||
var newUser,
|
||||
user;
|
||||
|
||||
return canThis(options.context).add.user().then(function () {
|
||||
return utils.checkObject(object, docName).then(function (checkedUserData) {
|
||||
newUser = checkedUserData.users[0];
|
||||
|
||||
if (newUser.email) {
|
||||
newUser.name = object.users[0].email.substring(0, newUser.email.indexOf("@"));
|
||||
newUser.password = globalUtils.uid(50);
|
||||
newUser.status = 'invited';
|
||||
// TODO: match user role with db and enforce permissions
|
||||
newUser.role = 3;
|
||||
} else {
|
||||
return when.reject(new errors.BadRequestError('No email provided.'));
|
||||
}
|
||||
}).then(function () {
|
||||
return dataProvider.User.getByEmail(newUser.email);
|
||||
}).then(function (foundUser) {
|
||||
if (!foundUser) {
|
||||
return dataProvider.User.add(newUser, options);
|
||||
} else {
|
||||
// only invitations for already invited users are resent
|
||||
if (foundUser.toJSON().status === 'invited') {
|
||||
return foundUser;
|
||||
} 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 * ONE_DAY),
|
||||
dbHash = response.settings[0].value;
|
||||
return dataProvider.User.generateResetToken(user.email, expires, dbHash);
|
||||
}).then(function (resetToken) {
|
||||
var baseUrl = config().forceAdminSSL ? (config().urlSSL || config().url) : config().url,
|
||||
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
|
||||
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + resetToken + '/',
|
||||
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
|
||||
payload = {
|
||||
mail: [{
|
||||
message: {
|
||||
to: user.email,
|
||||
subject: 'Invitation',
|
||||
html: '<p><strong>Hello!</strong></p>' +
|
||||
'<p>You have been invited to ' + siteLink + '.</p>' +
|
||||
'<p>Please follow the link to sign up and publish your ideas:<br><br>' + resetLink + '</p>' +
|
||||
'<p>Ghost</p>'
|
||||
},
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
return mail.send(payload);
|
||||
}).then(function () {
|
||||
return when.resolve({users: [user]});
|
||||
}).otherwise(function (error) {
|
||||
if (error && error.type === 'EmailError') {
|
||||
error.message = 'Error sending email: ' + error.message + ' Please check your email settings and resend the invitation.';
|
||||
}
|
||||
return when.reject(error);
|
||||
});
|
||||
}, function () {
|
||||
return when.reject(new errors.NoPermissionError('You do not have permission to add a users.'));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Register
|
||||
* Allow to register a user using the API without beeing authenticated in. Needed to set up the first user.
|
||||
|
@ -9,7 +9,7 @@ function BadRequestError(message) {
|
||||
}
|
||||
|
||||
BadRequestError.prototype = Object.create(Error.prototype);
|
||||
BadRequestError.prototype.name = "BadRequestError";
|
||||
BadRequestError.prototype.name = 'BadRequestError';
|
||||
|
||||
|
||||
module.exports = BadRequestError;
|
@ -9,7 +9,7 @@ function EmailError(message) {
|
||||
}
|
||||
|
||||
EmailError.prototype = Object.create(Error.prototype);
|
||||
EmailError.prototype.name = "EmailError";
|
||||
EmailError.prototype.name = 'EmailError';
|
||||
|
||||
|
||||
module.exports = EmailError;
|
@ -9,7 +9,7 @@ function InternalServerError(message) {
|
||||
}
|
||||
|
||||
InternalServerError.prototype = Object.create(Error.prototype);
|
||||
InternalServerError.prototype.name = "InternalServerError";
|
||||
InternalServerError.prototype.name = 'InternalServerError';
|
||||
|
||||
|
||||
module.exports = InternalServerError;
|
@ -9,7 +9,7 @@ function NoPermissionError(message) {
|
||||
}
|
||||
|
||||
NoPermissionError.prototype = Object.create(Error.prototype);
|
||||
NoPermissionError.prototype.name = "NoPermissionError";
|
||||
NoPermissionError.prototype.name = 'NoPermissionError';
|
||||
|
||||
|
||||
module.exports = NoPermissionError;
|
@ -9,7 +9,7 @@ function NotFoundError(message) {
|
||||
}
|
||||
|
||||
NotFoundError.prototype = Object.create(Error.prototype);
|
||||
NotFoundError.prototype.name = "NotFoundError";
|
||||
NotFoundError.prototype.name = 'NotFoundError';
|
||||
|
||||
|
||||
module.exports = NotFoundError;
|
@ -9,7 +9,7 @@ function RequestEntityTooLargeError(message) {
|
||||
}
|
||||
|
||||
RequestEntityTooLargeError.prototype = Object.create(Error.prototype);
|
||||
RequestEntityTooLargeError.prototype.name = "RequestEntityTooLargeError";
|
||||
RequestEntityTooLargeError.prototype.name = 'RequestEntityTooLargeError';
|
||||
|
||||
|
||||
module.exports = RequestEntityTooLargeError;
|
@ -9,7 +9,7 @@ function UnauthorizedError(message) {
|
||||
}
|
||||
|
||||
UnauthorizedError.prototype = Object.create(Error.prototype);
|
||||
UnauthorizedError.prototype.name = "UnauthorizedError";
|
||||
UnauthorizedError.prototype.name = 'UnauthorizedError';
|
||||
|
||||
|
||||
module.exports = UnauthorizedError;
|
@ -9,7 +9,7 @@ function UnsupportedMediaTypeError(message) {
|
||||
}
|
||||
|
||||
UnsupportedMediaTypeError.prototype = Object.create(Error.prototype);
|
||||
UnsupportedMediaTypeError.prototype.name = "UnsupportedMediaTypeError";
|
||||
UnsupportedMediaTypeError.prototype.name = 'UnsupportedMediaTypeError';
|
||||
|
||||
|
||||
module.exports = UnsupportedMediaTypeError;
|
@ -12,7 +12,7 @@ function ValidationError(message, offendingProperty) {
|
||||
}
|
||||
|
||||
ValidationError.prototype = Object.create(Error.prototype);
|
||||
ValidationError.prototype.name = "ValidationError";
|
||||
ValidationError.prototype.name = 'ValidationError';
|
||||
|
||||
|
||||
module.exports = ValidationError;
|
||||
|
@ -1,44 +1,10 @@
|
||||
var oauth2orize = require('oauth2orize'),
|
||||
models = require('../models'),
|
||||
models = require('../models'),
|
||||
utils = require('../utils'),
|
||||
|
||||
oauth;
|
||||
|
||||
|
||||
/**
|
||||
* Return a random int, used by `utils.uid()`
|
||||
*
|
||||
* @param {Number} min
|
||||
* @param {Number} max
|
||||
* @return {Number}
|
||||
* @api private
|
||||
*/
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique identifier with the given `len`.
|
||||
*
|
||||
* utils.uid(10);
|
||||
* // => "FDaS435D2z"
|
||||
*
|
||||
* @param {Number} len
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
function uid(len) {
|
||||
var buf = [],
|
||||
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
charlen = chars.length,
|
||||
i;
|
||||
|
||||
for (i = 1; i < len; i = i + 1) {
|
||||
buf.push(chars[getRandomInt(0, charlen - 1)]);
|
||||
}
|
||||
|
||||
return buf.join('');
|
||||
}
|
||||
|
||||
oauth = {
|
||||
|
||||
init: function (oauthServer) {
|
||||
@ -65,8 +31,8 @@ oauth = {
|
||||
return models.User.check({email: username, password: password}).then(function (user) {
|
||||
|
||||
//Everything validated, return the access- and refreshtoken
|
||||
var accessToken = uid(256),
|
||||
refreshToken = uid(256),
|
||||
var accessToken = utils.uid(256),
|
||||
refreshToken = utils.uid(256),
|
||||
accessExpires = Date.now() + 3600 * 1000,
|
||||
refreshExpires = Date.now() + 3600 * 24 * 1000;
|
||||
|
||||
@ -95,7 +61,7 @@ oauth = {
|
||||
return done(null, false);
|
||||
} else {
|
||||
var token = model.toJSON(),
|
||||
accessToken = uid(256),
|
||||
accessToken = utils.uid(256),
|
||||
accessExpires = Date.now() + 3600 * 1000;
|
||||
|
||||
if (token.expires > Date.now()) {
|
||||
|
@ -116,7 +116,6 @@ User = ghostBookshelf.Model.extend({
|
||||
* **See:** [ghostBookshelf.Model.add](base.js.html#Add)
|
||||
*/
|
||||
add: function (data, options) {
|
||||
|
||||
var self = this,
|
||||
// Clone the _user so we don't expose the hashed password unnecessarily
|
||||
userData = this.filterData(data);
|
||||
@ -130,11 +129,6 @@ User = ghostBookshelf.Model.extend({
|
||||
*/
|
||||
return validatePasswordLength(userData.password).then(function () {
|
||||
return self.forge().fetch();
|
||||
}).then(function (user) {
|
||||
// Check if user exists
|
||||
if (user) {
|
||||
return when.reject(new Error('A user is already registered. Only one user for now!'));
|
||||
}
|
||||
}).then(function () {
|
||||
// Generate a new password hash
|
||||
return generatePasswordHash(data.password);
|
||||
@ -150,31 +144,16 @@ User = ghostBookshelf.Model.extend({
|
||||
|
||||
// Assign the userData to our created user so we can pass it back
|
||||
userData = addedUser;
|
||||
// Add this user to the admin role (assumes admin = role_id: 1)
|
||||
return userData.roles().attach(1);
|
||||
if (!data.role) {
|
||||
// TODO: needs change when owner role is introduced and setup is changed
|
||||
data.role = 1;
|
||||
}
|
||||
return userData.roles().attach(data.role);
|
||||
}).then(function (addedUserRole) {
|
||||
/*jshint unused:false*/
|
||||
// find and return the added user
|
||||
return self.findOne({id: userData.id}, options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Temporarily replacing the function below with another one that checks
|
||||
* whether there's anyone registered at all. This is due to #138
|
||||
* @author javorszky
|
||||
*/
|
||||
|
||||
// return this.forge({email: userData.email}).fetch().then(function (user) {
|
||||
// if (user !== null) {
|
||||
// return when.reject(new Error('A user with that email address already exists.'));
|
||||
// }
|
||||
// return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) {
|
||||
// userData.password = hash;
|
||||
// ghostBookshelf.Model.add.call(UserRole, userRoles);
|
||||
// return ghostBookshelf.Model.add.call(User, userData);
|
||||
// }, errors.logAndThrowError);
|
||||
// }, errors.logAndThrowError);
|
||||
|
||||
},
|
||||
|
||||
permissable: function (userModelOrId, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
||||
@ -233,8 +212,11 @@ User = ghostBookshelf.Model.extend({
|
||||
check: function (object) {
|
||||
var self = this,
|
||||
s;
|
||||
|
||||
return this.getByEmail(object.email).then(function (user) {
|
||||
if (!user || user.get('status') === 'invited') {
|
||||
return when.reject(new Error('NotFound'));
|
||||
}
|
||||
|
||||
if (user.get('status') !== 'locked') {
|
||||
return nodefn.call(bcrypt.compare, object.password, user.get('password')).then(function (matched) {
|
||||
if (!matched) {
|
||||
@ -296,6 +278,10 @@ User = ghostBookshelf.Model.extend({
|
||||
|
||||
generateResetToken: function (email, expires, dbHash) {
|
||||
return this.getByEmail(email).then(function (foundUser) {
|
||||
if (!foundUser) {
|
||||
return when.reject(new Error('NotFound'));
|
||||
}
|
||||
|
||||
var hash = crypto.createHash('sha256'),
|
||||
text = "";
|
||||
|
||||
@ -398,8 +384,8 @@ User = ghostBookshelf.Model.extend({
|
||||
|
||||
gravatarLookup: function (userData) {
|
||||
var gravatarUrl = '//www.gravatar.com/avatar/' +
|
||||
crypto.createHash('md5').update(userData.email.toLowerCase().trim()).digest('hex') +
|
||||
"?d=404",
|
||||
crypto.createHash('md5').update(userData.email.toLowerCase().trim()).digest('hex') +
|
||||
"?d=404",
|
||||
checkPromise = when.defer();
|
||||
|
||||
http.get('http:' + gravatarUrl, function (res) {
|
||||
@ -427,12 +413,9 @@ User = ghostBookshelf.Model.extend({
|
||||
var userWithEmail = users.find(function (user) {
|
||||
return user.get('email').toLowerCase() === email.toLowerCase();
|
||||
});
|
||||
|
||||
if (userWithEmail) {
|
||||
return when.resolve(userWithEmail);
|
||||
}
|
||||
|
||||
return when.reject(new Error('NotFound'));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -22,6 +22,7 @@ apiRoutes = function (middleware) {
|
||||
router.get('/ghost/api/v0.1/users/:id/', api.http(api.users.read));
|
||||
router.put('/ghost/api/v0.1/users/password/', api.http(api.users.changePassword));
|
||||
router.put('/ghost/api/v0.1/users/:id/', api.http(api.users.edit));
|
||||
router.post('/ghost/api/v0.1/users/', api.http(api.users.invite));
|
||||
router['delete']('/ghost/api/v0.1/users/:id/', api.http(api.users.destroy));
|
||||
|
||||
// ## Tags
|
||||
|
38
core/server/utils/index.js
Normal file
38
core/server/utils/index.js
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Return a random int, used by `utils.uid()`
|
||||
*
|
||||
* @param {Number} min
|
||||
* @param {Number} max
|
||||
* @return {Number}
|
||||
* @api private
|
||||
*/
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
var utils = {
|
||||
/**
|
||||
* Return a unique identifier with the given `len`.
|
||||
*
|
||||
* utils.uid(10);
|
||||
* // => "FDaS435D2z"
|
||||
*
|
||||
* @param {Number} len
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
uid: function (len) {
|
||||
var buf = [],
|
||||
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
charlen = chars.length,
|
||||
i;
|
||||
|
||||
for (i = 1; i < len; i = i + 1) {
|
||||
buf.push(chars[getRandomInt(0, charlen - 1)]);
|
||||
}
|
||||
|
||||
return buf.join('');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = utils;
|
@ -150,15 +150,6 @@ describe('User Model', function run() {
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can\'t add second', function (done) {
|
||||
var userData = testUtils.DataGenerator.forModel.users[1];
|
||||
|
||||
return UserModel.add(userData, {user: 1}).then(done, function (failure) {
|
||||
failure.message.should.eql('A user is already registered. Only one user for now!');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can findAll', function (done) {
|
||||
|
||||
UserModel.findAll().then(function (results) {
|
||||
|
Loading…
Reference in New Issue
Block a user