🎨 Separate invites from user

refs #7420
- remove invite logic from user
- add invite model and adapt affected logic for inviting team members
This commit is contained in:
kirrg001 2016-09-21 16:48:14 +02:00
parent 6d092ada99
commit b79a18ca8f
22 changed files with 1008 additions and 495 deletions

View File

@ -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'),
@ -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) {
@ -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,
@ -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,
@ -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() {
@ -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;
@ -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) {
@ -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'));
@ -519,8 +543,8 @@ authentication = {
function revokeToken(options) {
var providers = [
dataProvider.Refreshtoken,
dataProvider.Accesstoken
models.Refreshtoken,
models.Accesstoken
],
response = {token: options.token};

View File

@ -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'),
@ -291,7 +292,8 @@ module.exports = {
authentication: authentication,
uploads: uploads,
slack: slack,
themes: themes
themes: themes,
invites: invites
};
/**

208
core/server/api/invites.js Normal file
View File

@ -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;

View File

@ -3,65 +3,16 @@
var Promise = require('bluebird'),
_ = require('lodash'),
dataProvider = require('../models'),
settings = require('./settings'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
utils = require('./utils'),
globalUtils = require('../utils'),
config = require('../config'),
mail = require('./../mail'),
apiMail = require('./mail'),
pipeline = require('../utils/pipeline'),
i18n = require('../i18n'),
docName = 'users',
// TODO: implement created_by, updated_by
allowedIncludes = ['count.posts', 'permissions', 'roles', 'roles.permissions'],
users,
sendInviteEmail;
users;
sendInviteEmail = function sendInviteEmail(user) {
var emailData;
return Promise.join(
users.read({id: user.created_by, context: {internal: true}}),
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.get('forceAdminSSL') ? (config.get('urlSSL') || config.get('url')) : config.get('url');
emailData.resetLink = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + globalUtils.encodeBase64URLsafe(resetToken) + '/';
return mail.utils.generateContent({data: emailData, template: 'invite-user'});
}).then(function (emailContent) {
var payload = {
mail: [{
message: {
to: user.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}});
});
};
/**
* ### Users API Methods
*
@ -245,120 +196,6 @@ users = {
});
},
/**
* ## Add user
* The newly added user is invited to join the blog via email.
* @param {User} object the user to create
* @param {{context}} options
* @returns {Promise<User>} Newly created user
*/
add: function add(object, options) {
var tasks;
/**
* ### Handle Permissions
* We need to be an authorised user to perform this action
* @param {Object} options
* @returns {Object} options
*/
function handlePermissions(options) {
var newUser = options.data.users[0];
return canThis(options.context).add.user(options.data).then(function () {
if (newUser.roles && newUser.roles[0]) {
var roleId = parseInt(newUser.roles[0].id || newUser.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.users.notAllowedToCreateOwner')));
}
return canThis(options.context).assign.role(role);
}).then(function () {
return options;
});
}
return options;
}).catch(function handleError(error) {
return errors.formatAndRejectAPIError(error, i18n.t('errors.api.users.noPermissionToAddUser'));
});
}
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function doQuery(options) {
var newUser = options.data.users[0],
user;
if (newUser.email) {
newUser.name = newUser.email.substring(0, newUser.email.indexOf('@'));
newUser.password = globalUtils.uid(50);
newUser.status = 'invited';
} else {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.users.noEmailProvided')));
}
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.get('status') === 'invited' || foundUser.get('status') === 'invited-pending') {
return foundUser;
} else {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.users.userAlreadyRegistered')));
}
}
}).then(function (invitedUser) {
user = invitedUser.toJSON(options);
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) {
user = editedUser.toJSON(options);
});
}
}).then(function () {
return Promise.resolve({users: [user]});
}).catch(function (error) {
if (error && error.errorType === 'EmailError') {
error.message = i18n.t('errors.api.users.errorSendingEmail.error', {message: error.message}) + ' ' +
i18n.t('errors.api.users.errorSendingEmail.help');
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, status: 'all'}, options).then(function (user) {
return {users: [user]};
});
});
}
return Promise.reject(error);
});
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName),
handlePermissions,
utils.convertOptions(allowedIncludes),
doQuery
];
return pipeline(tasks, object, options);
},
/**
* ## Destroy
* @param {{id, context}} options

View File

@ -289,6 +289,31 @@
"name": "Delete subscribers",
"action_type": "destroy",
"object_type": "subscriber"
},
{
"name": "Browse invites",
"action_type": "browse",
"object_type": "invite"
},
{
"name": "Read invites",
"action_type": "read",
"object_type": "invite"
},
{
"name": "Add invites",
"action_type": "add",
"object_type": "invite"
},
{
"name": "Edit invites",
"action_type": "edit",
"object_type": "invite"
},
{
"name": "Delete invites",
"action_type": "destroy",
"object_type": "invite"
}
]
}
@ -318,7 +343,8 @@
"user": "all",
"role": "all",
"client": "all",
"subscriber": "all"
"subscriber": "all",
"invite": "all"
},
"Editor": {
"post": "all",

View File

@ -215,5 +215,21 @@ module.exports = {
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'integer', nullable: true}
},
invites: {
id: {type: 'increments', nullable: false, primary: true},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'pending', validations: {isIn: [['pending', 'sent']]}},
token: {type: 'string', maxlength: 191, nullable: false, unique: true},
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
expires: {type: 'bigInteger', nullable: false},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'integer', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'integer', nullable: true}
},
invites_roles: {
id: {type: 'increments', nullable: false, primary: true},
role_id: {type: 'integer', nullable: false},
invite_id: {type: 'integer', nullable: false}
}
};

View File

@ -388,6 +388,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
findOne: function findOne(data, options) {
data = this.filterData(data);
options = this.filterOptions(options, 'findOne');
// We pass include to forge so that toJSON has access
return this.forge(data, {include: options.include}).fetch(options);
},

View File

@ -28,7 +28,8 @@ models = [
'settings',
'subscriber',
'tag',
'user'
'user',
'invite'
];
function init() {

View File

@ -0,0 +1,128 @@
var ghostBookshelf = require('./base'),
globalUtils = require('../utils'),
crypto = require('crypto'),
_ = require('lodash'),
Promise = require('bluebird'),
Invite,
Invites;
Invite = ghostBookshelf.Model.extend({
tableName: 'invites',
toJSON: function (options) {
options = options || {};
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
delete attrs.token;
return attrs;
},
roles: function roles() {
return this.belongsToMany('Role');
}
}, {
orderDefaultOptions: function orderDefaultOptions() {
return {};
},
processOptions: function processOptions(options) {
return options;
},
filterData: function filterData(data) {
var permittedAttributes = this.prototype.permittedAttributes(),
filteredData;
permittedAttributes.push('roles');
filteredData = _.pick(data, permittedAttributes);
return filteredData;
},
permittedOptions: function permittedOptions(methodName) {
var options = ghostBookshelf.Model.permittedOptions(),
validOptions = {
findOne: ['withRelated'],
edit: ['withRelated'],
findPage: ['withRelated']
};
if (validOptions[methodName]) {
options = options.concat(validOptions[methodName]);
}
return options;
},
/**
* @TODO: can't use base class, because:
* options.withRelated = _.union(options.withRelated, options.include); is missing
* there are some weird self implementations in each model
* so adding this line, will destroy other models, because they rely on something else
* FIX ME!!!!!
*/
findOne: function findOne(data, options) {
options = options || {};
options = this.filterOptions(options, 'findOne');
data = this.filterData(data, 'findOne');
options.withRelated = _.union(options.withRelated, options.include);
var invite = this.forge(data, {include: options.include});
return invite.fetch(options);
},
add: function add(data, options) {
var hash = crypto.createHash('sha256'),
text = '',
roles = data.roles,
self = this,
invite;
options = this.filterOptions(options, 'add');
options.withRelated = _.union(options.withRelated, options.include);
data.expires = Date.now() + globalUtils.ONE_WEEK_MS;
data.status = 'pending';
// @TODO: call a util fn?
hash.update(String(data.expires));
hash.update(data.email.toLocaleLowerCase());
text += [data.expires, data.email, hash.digest('base64')].join('|');
data.token = new Buffer(text).toString('base64');
delete data.roles;
return ghostBookshelf.Model.add.call(this, data, options)
.then(function (_invite) {
invite = _invite;
return Promise.resolve(roles)
.then(function then(roles) {
roles = _.map(roles, function mapper(role) {
if (_.isString(role)) {
return parseInt(role, 10);
} else if (_.isNumber(role)) {
return role;
} else {
return parseInt(role.id, 10);
}
});
return invite.roles().attach(roles, options);
});
})
.then(function () {
return self.findOne({id: invite.id}, options);
});
}
});
Invites = ghostBookshelf.Collection.extend({
model: Invite
});
module.exports = {
Invite: ghostBookshelf.model('Invite', Invite),
Invites: ghostBookshelf.collection('Invites', Invites)
};

View File

@ -17,7 +17,6 @@ var _ = require('lodash'),
tokenSecurity = {},
activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'],
invitedStates = ['invited', 'invited-pending'],
User,
Users;
@ -201,8 +200,7 @@ User = ghostBookshelf.Model.extend({
// This is the only place that 'options.where' is set now
options.where = {statements: []};
var allStates = activeStates.concat(invitedStates),
value;
var allStates = activeStates, value;
// Filter on the status. A status of 'all' translates to no filter since we want all statuses
if (options.status !== 'all') {
@ -212,8 +210,6 @@ User = ghostBookshelf.Model.extend({
if (options.status === 'active') {
value = activeStates;
} else if (options.status === 'invited') {
value = invitedStates;
} else if (options.status === 'all') {
value = allStates;
} else {
@ -293,8 +289,6 @@ User = ghostBookshelf.Model.extend({
if (status === 'active') {
query.query('whereIn', 'status', activeStates);
} else if (status === 'invited') {
query.query('whereIn', 'status', invitedStates);
} else if (status !== 'all') {
query.query('where', {status: options.status});
}
@ -302,7 +296,6 @@ User = ghostBookshelf.Model.extend({
options = this.filterOptions(options, 'findOne');
delete options.include;
options.include = optInc;
return query.fetch(options);
},
@ -545,11 +538,7 @@ User = ghostBookshelf.Model.extend({
if (!user) {
return Promise.reject(new errors.NotFoundError(i18n.t('errors.models.user.noUserWithEnteredEmailAddr')));
}
if (user.get('status') === 'invited' || user.get('status') === 'invited-pending' ||
user.get('status') === 'inactive'
) {
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.models.user.userIsInactive')));
}
if (user.get('status') !== 'locked') {
return bcryptCompare(object.password, user.get('password')).then(function then(matched) {
if (!matched) {

View File

@ -64,7 +64,6 @@ apiRoutes = function apiRoutes(middleware) {
router.put('/users/password', authenticatePrivate, api.http(api.users.changePassword));
router.put('/users/owner', authenticatePrivate, api.http(api.users.transferOwnership));
router.put('/users/:id', authenticatePrivate, api.http(api.users.edit));
router.post('/users', authenticatePrivate, api.http(api.users.add));
router.del('/users/:id', authenticatePrivate, api.http(api.users.destroy));
// ## Tags
@ -166,6 +165,12 @@ apiRoutes = function apiRoutes(middleware) {
api.http(api.uploads.add)
);
// ## Invites
router.get('/invites', authenticatePrivate, api.http(api.invites.browse));
router.get('/invites/:id', authenticatePrivate, api.http(api.invites.read));
router.post('/invites', authenticatePrivate, api.http(api.invites.add));
router.del('/invites/:id', authenticatePrivate, api.http(api.invites.destroy));
// API Router middleware
router.use(middleware.api.errorHandler);

View File

@ -292,6 +292,9 @@
"setupUnableToRun": "Database missing fixture data. Please reset database and try again.",
"setupMustBeCompleted": "Setup must be completed before making this request.",
"noEmailProvided": "No email provided.",
"noTokenProvided": "No token provided.",
"noPasswordProvided": "No password provided.",
"noNameProvided": "No name provided.",
"invalidEmailReceived": "The server did not receive a valid email",
"setupAlreadyCompleted": "Setup has already been completed.",
"unableToSendWelcomeEmail": "Unable to send welcome email, your blog will continue to function.",
@ -373,14 +376,9 @@
"cannotChangeOwnRole": "You cannot change your own role.",
"cannotChangeOwnersRole": "Cannot change Owner's role",
"noPermissionToEditUser": "You do not have permission to edit this user",
"notAllowedToCreateOwner": "Not allowed to create an owner user.",
"noPermissionToAddUser": "You do not have permission to add this user",
"noEmailProvided": "No email provided.",
"userAlreadyRegistered": "User is already registered.",
"errorSendingEmail": {
"error": "Error sending email: {message}",
"help": "Please check your email settings and resend the invitation."
},
"noPermissionToDestroyUser": "You do not have permission to destroy this user.",
"noPermissionToChangeUsersPwd": "You do not have permission to change the password for this user"
},
@ -388,6 +386,17 @@
"noPermissionToCall": "You do not have permission to {method} {docName}",
"noRootKeyProvided": "No root key ('{docName}') provided.",
"invalidIdProvided": "Invalid id provided."
},
"invites": {
"inviteNotFound": "Invite not found.",
"inviteExpired": "Invite is expired.",
"emailIsRequired": "E-Mail is required.",
"roleIsRequired": "Role is required",
"errorSendingEmail": {
"error": "Error sending email: {message}",
"help": "Please check your email settings and resend the invitation."
},
"notAllowedToInviteOwner": "Not allowed to invire an owner user."
}
},
"data": {

View File

@ -1,19 +1,18 @@
var testUtils = require('../../utils'),
should = require('should'),
_ = require('lodash'),
sinon = require('sinon'),
Promise = require('bluebird'),
uid = require('../../../server/utils').uid,
Accesstoken,
Refreshtoken,
User,
// Stuff we are testing
AuthAPI = require('../../../server/api/authentication'),
mail = require('../../../server/api/mail'),
models = require('../../../server/models'),
errors = require('../../../server/errors'),
sandbox = sinon.sandbox.create(),
context = testUtils.context,
sandbox = sinon.sandbox.create();
Accesstoken,
Refreshtoken,
User;
describe('Authentication API', function () {
var testInvite = {
@ -207,7 +206,7 @@ describe('Authentication API', function () {
User = require('../../../server/models/user').User;
});
beforeEach(testUtils.setup('roles', 'owner', 'clients', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
beforeEach(testUtils.setup('invites', 'roles', 'owner', 'clients', 'settings', 'perms:setting', 'perms:mail', 'perms:init'));
it('should report that setup has been completed', function (done) {
AuthAPI.isSetup().then(function (result) {
@ -244,14 +243,84 @@ describe('Authentication API', function () {
}).catch(function (err) {
should.exist(err);
err.name.should.equal('UnauthorizedError');
err.statusCode.should.equal(401);
err.message.should.equal('Invalid token structure');
err.name.should.equal('NotFoundError');
err.statusCode.should.equal(404);
err.message.should.equal('Invite not found.');
done();
}).catch(done);
});
it('should allow an invitation to be accepted', function () {
var invite;
return models.Invite.add({email: '123@meins.de', roles: [1]}, _.merge({}, {include: ['roles']}, context.internal))
.then(function (_invite) {
invite = _invite;
invite.toJSON().roles.length.should.eql(1);
return models.Invite.edit({status: 'sent'}, _.merge({}, {id: invite.id}, context.internal));
})
.then(function () {
return AuthAPI.acceptInvitation({
invitation: [
{
token: invite.get('token'),
email: invite.get('email'),
name: invite.get('email'),
password: 'eightcharacterslong'
}
]
});
})
.then(function (res) {
should.exist(res.invitation[0].message);
return models.Invite.findOne({id: invite.id}, context.internal);
})
.then(function (_invite) {
should.not.exist(_invite);
return models.User.findOne({
email: invite.get('email')
}, _.merge({include: ['roles']}, context.internal));
})
.then(function (user) {
user.toJSON().roles.length.should.eql(1);
});
});
it('should not allow an invitation to be accepted: expired', function () {
var invite;
return models.Invite.add({email: '123@meins.de'}, context.internal)
.then(function (_invite) {
invite = _invite;
return models.Invite.edit({
status: 'sent',
expires: Date.now() - 10000}, _.merge({}, {id: invite.id}, context.internal));
})
.then(function () {
return AuthAPI.acceptInvitation({
invitation: [
{
token: invite.get('token'),
email: invite.get('email'),
name: invite.get('email'),
password: 'eightcharacterslong'
}
]
});
})
.then(function () {
throw new Error('should not pass the test: expected expired invitation');
})
.catch(function (err) {
should.exist(err);
(err instanceof errors.NotFoundError).should.eql(true);
err.message.should.eql('Invite is expired.');
});
});
it('should generate a password reset token', function (done) {
AuthAPI.generateResetToken(testGenerateReset).then(function (result) {
should.exist(result);
@ -320,25 +389,12 @@ describe('Authentication API', function () {
}).catch(done);
});
it('should know an email address has an active invitation', function (done) {
var user = {
name: 'test user',
email: 'invited@example.com',
password: '12345678',
status: 'invited'
},
options = {
context: {internal: true}
};
User.add(user, options).then(function (user) {
return AuthAPI.isInvitation({email: user.get('email')});
}).then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.true();
done();
}).catch(done);
it('should know an email address has an active invitation', function () {
return AuthAPI.isInvitation({email: testUtils.DataGenerator.forKnex.invites[0].email})
.then(function (response) {
should.exist(response);
response.invitation[0].valid.should.be.true();
});
});
it('should know an email address does not have an active invitation', function (done) {

View File

@ -0,0 +1,378 @@
var testUtils = require('../../utils'),
should = require('should'),
sinon = require('sinon'),
_ = require('lodash'),
Promise = require('bluebird'),
InvitesAPI = require('../../../server/api/invites'),
mail = require('../../../server/api/mail'),
errors = require('../../../server/errors'),
context = testUtils.context,
sandbox = sinon.sandbox.create();
describe('Invites API', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('invites', 'users:roles', 'perms:invite', 'perms:init'));
beforeEach(function () {
sandbox.stub(mail, 'send', function () {
return Promise.resolve();
});
});
afterEach(function () {
sandbox.restore();
});
after(testUtils.teardown);
describe('CRUD', function () {
describe('Add', function () {
it('add invite 1', function (done) {
InvitesAPI.add({
invites: [{email: 'kate+1@ghost.org', roles: [testUtils.roles.ids.editor]}]
}, _.merge({}, {include: ['roles']}, testUtils.context.owner))
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].roles.length.should.eql(1);
response.invites[0].roles[0].name.should.eql('Editor');
done();
}).catch(done);
});
it('add invite 2', function (done) {
InvitesAPI.add({
invites: [{email: 'kate+2@ghost.org', roles: [testUtils.roles.ids.author]}]
}, _.merge({}, {include: ['roles']}, testUtils.context.owner))
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].roles.length.should.eql(1);
response.invites[0].roles[0].name.should.eql('Author');
done();
}).catch(done);
});
it('add invite: empty invites object', function (done) {
InvitesAPI.add({invites: []}, _.merge({}, {include: ['roles']}, testUtils.context.owner))
.then(function () {
throw new Error('expected validation error');
})
.catch(function (err) {
should.exist(err);
done();
});
});
it('add invite: no email provided', function (done) {
InvitesAPI.add({invites: [{status: 'sent'}]}, _.merge({}, {include: ['roles']}, testUtils.context.owner))
.then(function () {
throw new Error('expected validation error');
})
.catch(function (err) {
(err instanceof errors.ValidationError).should.eql(true);
done();
});
});
});
describe('Browse', function () {
it('browse invites', function (done) {
InvitesAPI.browse(_.merge({}, {include: ['roles']}, testUtils.context.owner))
.then(function (response) {
response.invites.length.should.eql(2);
response.invites[0].status.should.eql('sent');
response.invites[0].email.should.eql('test1@ghost.org');
response.invites[0].roles.length.should.eql(1);
response.invites[0].roles[0].name.should.eql('Administrator');
response.invites[1].status.should.eql('sent');
response.invites[1].email.should.eql('test2@ghost.org');
response.invites[1].roles.length.should.eql(1);
response.invites[1].roles[0].name.should.eql('Author');
should.not.exist(response.invites[0].token);
should.exist(response.invites[0].expires);
should.not.exist(response.invites[1].token);
should.exist(response.invites[1].expires);
done();
}).catch(done);
});
});
describe('Read', function () {
it('read invites: not found', function (done) {
InvitesAPI.read(_.merge({}, testUtils.context.owner, {
email: 'not-existend@hey.org',
include: ['roles']
})).then(function () {
throw new Error('expected not found error for invite');
}).catch(function (err) {
(err instanceof errors.NotFoundError).should.eql(true);
done();
});
});
it('read invite', function (done) {
InvitesAPI.read(_.merge({}, {email: 'test1@ghost.org', include: ['roles']}, testUtils.context.owner))
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].roles.length.should.eql(1);
response.invites[0].roles[0].name.should.eql('Administrator');
done();
}).catch(done);
});
it('read invite', function (done) {
InvitesAPI.read(_.merge({}, testUtils.context.owner, {email: 'test2@ghost.org', include: ['roles']}))
.then(function (response) {
response.invites.length.should.eql(1);
response.invites[0].roles.length.should.eql(1);
response.invites[0].roles[0].name.should.eql('Author');
done();
}).catch(done);
});
});
describe('Destroy', function () {
it('destroy invite', function (done) {
InvitesAPI.destroy(_.merge({}, testUtils.context.owner, {id: 1, include: ['roles']}))
.then(function () {
return InvitesAPI.read(_.merge({}, testUtils.context.owner, {
email: 'test1@ghost.org',
include: ['roles']
})).catch(function (err) {
(err instanceof errors.NotFoundError).should.eql(true);
done();
});
}).catch(done);
});
it('destroy invite: id does not exist', function (done) {
InvitesAPI.destroy({context: {user: 1}, id: 100})
.then(function () {
throw new Error('expect error on destroy invite');
})
.catch(function (err) {
(err instanceof errors.NotFoundError).should.eql(true);
done();
});
});
});
});
describe('Permissions', function () {
function checkForErrorType(type, done) {
return function checkForErrorType(error) {
if (error.errorType) {
error.errorType.should.eql(type);
done();
} else {
done(error);
}
};
}
function checkAddResponse(response) {
should.exist(response);
should.exist(response.invites);
should.not.exist(response.meta);
response.invites.should.have.length(1);
testUtils.API.checkResponse(response.invites[0], 'invites', ['roles']);
response.invites[0].created_at.should.be.an.instanceof(Date);
}
describe('Owner', function () {
it('CANNOT add an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.owner]
}
]
}, context.owner).then(function () {
done(new Error('Owner should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can add an Admin', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.admin]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.owner)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Administrator');
done();
}).catch(done);
});
it('Can add an Editor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.editor]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.owner)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Editor');
done();
}).catch(done);
});
it('Can add an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.author]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.owner)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Author');
done();
}).catch(done);
});
it('Can add with role set as string', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.author.toString()]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.owner)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Author');
done();
}).catch(done);
});
});
describe('Admin', function () {
it('CANNOT add an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.owner]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.admin)).then(function () {
done(new Error('Admin should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can add an Admin', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.admin]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.admin)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Administrator');
done();
}).catch(done);
});
it('Can add an Editor', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.editor]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.admin)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Editor');
done();
}).catch(done);
});
it('Can add an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.author]
}
]
}, _.merge({}, {include: ['roles']}, testUtils.context.admin)).then(function (response) {
checkAddResponse(response);
response.invites[0].roles[0].name.should.equal('Author');
done();
}).catch(done);
});
});
describe('Editor', function () {
it('CANNOT add an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.owner]
}
]
}, context.editor).then(function () {
done(new Error('Editor should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT add an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.author]
}
]
}, context.editor).then(function () {
done(new Error('Editor should not be able to add an author'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
describe('Author', function () {
it('CANNOT add an Owner', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.owner]
}
]
}, context.author).then(function () {
done(new Error('Author should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT add an Author', function (done) {
InvitesAPI.add({
invites: [
{
email: 'kate+1@ghost.org',
roles: [testUtils.roles.ids.author]
}
]
}, context.author).then(function () {
done(new Error('Author should not be able to add an Author'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
});
});

View File

@ -1,19 +1,13 @@
var testUtils = require('../../utils'),
should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
_ = require('lodash'),
// Stuff we are testing
models = require('../../../server/models'),
UserAPI = require('../../../server/api/users'),
mail = require('../../../server/api/mail'),
db = require('../../../server/data/db'),
context = testUtils.context,
userIdFor = testUtils.users.ids,
roleIdFor = testUtils.roles.ids,
sandbox = sinon.sandbox.create();
roleIdFor = testUtils.roles.ids;
describe('Users API', function () {
// Keep the DB clean
@ -97,29 +91,6 @@ describe('Users API', function () {
}).catch(done);
});
it('No-auth CANNOT browse non-active users', function (done) {
UserAPI.browse({status: 'invited'}).then(function () {
done(new Error('Browse non-active users is not denied without authentication.'));
}, function () {
done();
}).catch(done);
});
it('Can browse invited/invited-pending (admin)', function (done) {
testUtils.fixtures.createInvitedUsers().then(function () {
UserAPI.browse(_.extend({}, testUtils.context.admin, {status: 'invited'})).then(function (response) {
should.exist(response);
testUtils.API.checkResponse(response, 'users');
should.exist(response.users);
response.users.should.have.length(3);
testUtils.API.checkResponse(response.users[0], 'user');
response.users[0].status.should.equal('invited-pending');
done();
}).catch(done);
});
});
it('Can browse all', function (done) {
UserAPI.browse(_.extend({}, testUtils.context.admin, {status: 'all'})).then(function (response) {
checkBrowseResponse(response, 7);
@ -491,208 +462,6 @@ describe('Users API', function () {
});
});
describe('Add', function () {
var newUser;
beforeEach(function () {
newUser = _.clone(testUtils.DataGenerator.forKnex.createUser(testUtils.DataGenerator.Content.users[4]));
sandbox.stub(mail, 'send', function () {
return Promise.resolve();
});
});
afterEach(function () {
sandbox.restore();
});
function checkAddResponse(response) {
should.exist(response);
should.exist(response.users);
should.not.exist(response.meta);
response.users.should.have.length(1);
testUtils.API.checkResponse(response.users[0], 'user', ['roles']);
response.users[0].created_at.should.be.an.instanceof(Date);
}
describe('Owner', function () {
it('CANNOT add an Owner', function (done) {
newUser.roles = [roleIdFor.owner];
// Owner cannot add owner
UserAPI.add({users: [newUser]}, _.extend({}, context.owner, {include: 'roles'}))
.then(function () {
done(new Error('Owner should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can add an Admin', function (done) {
// Can add admin
newUser.roles = [roleIdFor.admin];
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('Administrator');
done();
}).catch(done);
});
it('Can add an Editor', function (done) {
// Can add editor
newUser.roles = [roleIdFor.editor];
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('Editor');
done();
}).catch(done);
});
it('Can add an Author', function (done) {
// Can add author
newUser.roles = [roleIdFor.author];
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 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 () {
it('CANNOT add an Owner', function (done) {
newUser.roles = [roleIdFor.owner];
// Admin cannot add owner
UserAPI.add({users: [newUser]}, _.extend({}, context.admin, {include: 'roles'}))
.then(function () {
done(new Error('Admin should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can add an Admin', function (done) {
// Can add admin
newUser.roles = [roleIdFor.admin];
UserAPI.add({users: [newUser]}, _.extend({}, context.admin, {include: 'roles'}))
.then(function (response) {
checkAddResponse(response);
response.users[0].id.should.eql(8);
response.users[0].roles[0].name.should.equal('Administrator');
done();
}).catch(done);
});
it('Can add an Editor', function (done) {
// Can add editor
newUser.roles = [roleIdFor.editor];
UserAPI.add({users: [newUser]}, _.extend({}, context.admin, {include: 'roles'}))
.then(function (response) {
checkAddResponse(response);
response.users[0].id.should.eql(8);
response.users[0].roles[0].name.should.equal('Editor');
done();
}).catch(done);
});
it('Can add an Author', function (done) {
// Can add author
newUser.roles = [roleIdFor.author];
UserAPI.add({users: [newUser]}, _.extend({}, context.admin, {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 two users with the same local-part in their email addresses', function (done) {
newUser.roles = [roleIdFor.author];
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');
}).then(function () {
newUser.email = newUser.email.split('@')[0] + '@someotherdomain.com';
return UserAPI.add({users: [newUser]}, _.extend({}, context.owner, {include: 'roles'}))
.then(function (response) {
checkAddResponse(response);
response.users[0].id.should.eql(9);
response.users[0].roles[0].name.should.equal('Author');
done();
});
}).catch(done);
});
});
describe('Editor', function () {
it('CANNOT add an Owner', function (done) {
newUser.roles = [roleIdFor.owner];
// Editor cannot add owner
UserAPI.add({users: [newUser]}, _.extend({}, context.editor, {include: 'roles'}))
.then(function () {
done(new Error('Editor should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('Can add an Author', function (done) {
newUser.roles = [roleIdFor.author];
UserAPI.add({users: [newUser]}, _.extend({}, context.editor, {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('Author', function () {
it('CANNOT add an Owner', function (done) {
newUser.roles = [roleIdFor.owner];
// Admin cannot add owner
UserAPI.add({users: [newUser]}, _.extend({}, context.author, {include: 'roles'}))
.then(function () {
done(new Error('Author should not be able to add an owner'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT add an Author', function (done) {
newUser.roles = [roleIdFor.author];
UserAPI.add({users: [newUser]}, _.extend({}, context.author, {include: 'roles'}))
.then(function () {
done(new Error('Author should not be able to add an author'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
});
describe('Destroy', function () {
describe('General Tests', function () {
it('ensure posts get deleted', function (done) {

View File

@ -212,7 +212,7 @@ describe('Database Migration (special functions)', function () {
result.roles.at(3).get('name').should.eql('Owner');
// Permissions
result.permissions.length.should.eql(43);
result.permissions.length.should.eql(48);
result.permissions.toJSON().should.be.CompletePermissions();
done();

View File

@ -0,0 +1,26 @@
var testUtils = require('../../utils'),
should = require('should'),
models = require('../../../server/models');
describe('Invite Model', function () {
before(testUtils.teardown);
afterEach(testUtils.teardown);
describe('add invite', function () {
beforeEach(testUtils.setup());
it('create invite', function (done) {
models.Invite.add({
email: 'test@test.de'
}, testUtils.context.internal)
.then(function (invite) {
should.exist(invite);
should.exist(invite.get('token'));
should.exist(invite.get('expires'));
should.exist(invite.get('email'));
done();
})
.catch(done);
});
});
});

View File

@ -1363,9 +1363,9 @@ describe('Fixtures', function () {
clientOneStub.calledThrice.should.be.true();
clientAddStub.calledThrice.should.be.true();
permOneStub.callCount.should.eql(43);
permOneStub.callCount.should.eql(48);
permsAddStub.called.should.be.true();
permsAddStub.callCount.should.eql(43);
permsAddStub.callCount.should.eql(48);
permsAllStub.calledOnce.should.be.true();
rolesAllStub.calledOnce.should.be.true();
@ -1374,8 +1374,8 @@ describe('Fixtures', function () {
// Relations
modelMethodStub.filter.called.should.be.true();
// 26 permissions, 1 tag
modelMethodStub.filter.callCount.should.eql(28 + 1);
// 29 permissions, 1 tag
modelMethodStub.filter.callCount.should.eql(29 + 1);
modelMethodStub.find.called.should.be.true();
// 3 roles, 1 post
modelMethodStub.find.callCount.should.eql(3 + 1);

View File

@ -151,21 +151,21 @@ describe('Utils', function () {
fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) {
should.exist(result);
result.should.be.an.Object();
result.should.have.property('expected', 28);
result.should.have.property('done', 28);
result.should.have.property('expected', 29);
result.should.have.property('done', 29);
// Permissions & Roles
permsAllStub.calledOnce.should.be.true();
rolesAllStub.calledOnce.should.be.true();
dataMethodStub.filter.callCount.should.eql(28);
dataMethodStub.filter.callCount.should.eql(29);
dataMethodStub.find.callCount.should.eql(3);
fromItem.related.callCount.should.eql(28);
fromItem.findWhere.callCount.should.eql(28);
toItem[0].get.callCount.should.eql(56);
fromItem.related.callCount.should.eql(29);
fromItem.findWhere.callCount.should.eql(29);
toItem[0].get.callCount.should.eql(58);
fromItem.permissions.callCount.should.eql(28);
fromItem.attach.callCount.should.eql(28);
fromItem.permissions.callCount.should.eql(29);
fromItem.attach.callCount.should.eql(29);
fromItem.attach.calledWith(toItem).should.be.true();
done();

View File

@ -31,7 +31,8 @@ var _ = require('lodash'),
role: _.keys(schema.roles),
permission: _.keys(schema.permissions),
notification: ['type', 'message', 'status', 'id', 'dismissible', 'location'],
theme: ['uuid', 'name', 'version', 'active']
theme: ['uuid', 'name', 'version', 'active'],
invites: _(schema.invites).keys().without('token').value()
};
function getApiQuery(route) {

View File

@ -264,7 +264,9 @@ DataGenerator.forKnex = (function () {
roles,
users,
roles_users,
clients;
clients,
invites,
invites_roles;
function createBasic(overrides) {
var newObj = _.cloneDeep(overrides);
@ -392,6 +394,19 @@ DataGenerator.forKnex = (function () {
});
}
function createInvite(overrides) {
var newObj = _.cloneDeep(overrides);
return _.defaults(newObj, {
token: uuid.v4(),
email: 'test@ghost.org',
expires: Date.now() + (60 * 1000),
created_by: 1,
created_at: new Date(),
status: 'sent'
});
}
posts = [
createPost(DataGenerator.Content.posts[0]),
createPost(DataGenerator.Content.posts[1]),
@ -457,6 +472,16 @@ DataGenerator.forKnex = (function () {
createAppField(DataGenerator.Content.app_fields[1])
];
invites = [
createInvite({email: 'test1@ghost.org'}),
createInvite({email: 'test2@ghost.org'})
];
invites_roles = [
{invite_id: 1, role_id: 1},
{invite_id: 2, role_id: 3}
];
return {
createPost: createPost,
createGenericPost: createGenericPost,
@ -473,7 +498,10 @@ DataGenerator.forKnex = (function () {
createAppSetting: createAppSetting,
createToken: createToken,
createSubscriber: createBasic,
createInvite: createInvite,
invites: invites,
invites_roles: invites_roles,
posts: posts,
tags: tags,
posts_tags: posts_tags,

View File

@ -187,6 +187,7 @@ fixtures = {
}));
});
},
insertRoles: function insertRoles() {
return db.knex('roles').insert(DataGenerator.forKnex.roles);
},
@ -383,6 +384,13 @@ fixtures = {
insertAccessToken: function insertAccessToken(override) {
return db.knex('accesstokens').insert(DataGenerator.forKnex.createToken(override));
},
insertInvites: function insertInvites() {
return db.knex('invites').insert(DataGenerator.forKnex.invites)
.then(function () {
return db.knex('invites_roles').insert(DataGenerator.forKnex.invites_roles);
});
}
};
@ -434,7 +442,8 @@ toDoList = {
return function permissionsForObj() { return fixtures.permissionsFor(obj); };
},
clients: function insertClients() { return fixtures.insertClients(); },
filter: function createFilterParamFixtures() { return filterData(DataGenerator); }
filter: function createFilterParamFixtures() { return filterData(DataGenerator); },
invites: function insertInvites() { return fixtures.insertInvites(); }
};
/**