diff --git a/core/server/api/authentication.js b/core/server/api/authentication.js index 5cfb42d1316..09f1e6a0625 100644 --- a/core/server/api/authentication.js +++ b/core/server/api/authentication.js @@ -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} */ 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}; diff --git a/core/server/api/index.js b/core/server/api/index.js index e5712a02a03..7e583b66ea0 100644 --- a/core/server/api/index.js +++ b/core/server/api/index.js @@ -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 }; /** diff --git a/core/server/api/invites.js b/core/server/api/invites.js new file mode 100644 index 00000000000..e6191ca6ace --- /dev/null +++ b/core/server/api/invites.js @@ -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; diff --git a/core/server/api/users.js b/core/server/api/users.js index 37b4e02264f..8513f73d08b 100644 --- a/core/server/api/users.js +++ b/core/server/api/users.js @@ -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; - -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; + /** * ### 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} 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 diff --git a/core/server/data/migration/fixtures/fixtures.json b/core/server/data/migration/fixtures/fixtures.json index db5b36c3bc2..b0c20ad39be 100644 --- a/core/server/data/migration/fixtures/fixtures.json +++ b/core/server/data/migration/fixtures/fixtures.json @@ -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", diff --git a/core/server/data/schema/schema.js b/core/server/data/schema/schema.js index c0af4ca2aaa..ffff1da8e7f 100644 --- a/core/server/data/schema/schema.js +++ b/core/server/data/schema/schema.js @@ -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} } }; diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 9a8027c075c..f0709c5f326 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -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); }, diff --git a/core/server/models/index.js b/core/server/models/index.js index 95d17bfa129..d429accfcde 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -28,7 +28,8 @@ models = [ 'settings', 'subscriber', 'tag', - 'user' + 'user', + 'invite' ]; function init() { diff --git a/core/server/models/invite.js b/core/server/models/invite.js new file mode 100644 index 00000000000..2908522d965 --- /dev/null +++ b/core/server/models/invite.js @@ -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) +}; diff --git a/core/server/models/user.js b/core/server/models/user.js index 25cab4b41a7..eacfe6f4a90 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -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) { diff --git a/core/server/routes/api.js b/core/server/routes/api.js index 9fdd8a706ec..e6420eb82ed 100644 --- a/core/server/routes/api.js +++ b/core/server/routes/api.js @@ -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); diff --git a/core/server/translations/en.json b/core/server/translations/en.json index 7b5c0c55ff3..4b12346dcad 100644 --- a/core/server/translations/en.json +++ b/core/server/translations/en.json @@ -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": { diff --git a/core/test/integration/api/api_authentication_spec.js b/core/test/integration/api/api_authentication_spec.js index d95dd286c26..d1a4285d847 100644 --- a/core/test/integration/api/api_authentication_spec.js +++ b/core/test/integration/api/api_authentication_spec.js @@ -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) { diff --git a/core/test/integration/api/api_invites_spec.js b/core/test/integration/api/api_invites_spec.js new file mode 100644 index 00000000000..cc13f1cddcb --- /dev/null +++ b/core/test/integration/api/api_invites_spec.js @@ -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)); + }); + }); + }); +}); diff --git a/core/test/integration/api/api_users_spec.js b/core/test/integration/api/api_users_spec.js index 1b5089e647c..9a8ada9a1ea 100644 --- a/core/test/integration/api/api_users_spec.js +++ b/core/test/integration/api/api_users_spec.js @@ -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) { diff --git a/core/test/integration/migration_spec.js b/core/test/integration/migration_spec.js index ebbb5531e04..b6063393f5a 100644 --- a/core/test/integration/migration_spec.js +++ b/core/test/integration/migration_spec.js @@ -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(); diff --git a/core/test/integration/model/model_invite_spec.js b/core/test/integration/model/model_invite_spec.js new file mode 100644 index 00000000000..221a669ccbb --- /dev/null +++ b/core/test/integration/model/model_invite_spec.js @@ -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); + }); + }); +}); diff --git a/core/test/unit/migration_fixture_spec.js b/core/test/unit/migration_fixture_spec.js index f576a174f51..8c2c2ab75da 100644 --- a/core/test/unit/migration_fixture_spec.js +++ b/core/test/unit/migration_fixture_spec.js @@ -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); diff --git a/core/test/unit/migration_fixture_utils_spec.js b/core/test/unit/migration_fixture_utils_spec.js index 4bbe39d86a0..92267f1cfc1 100644 --- a/core/test/unit/migration_fixture_utils_spec.js +++ b/core/test/unit/migration_fixture_utils_spec.js @@ -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(); diff --git a/core/test/utils/api.js b/core/test/utils/api.js index c534f9debf6..d3f1ad103e1 100644 --- a/core/test/utils/api.js +++ b/core/test/utils/api.js @@ -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) { diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index 6f9f72fe24e..a5ab2b48f92 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -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, diff --git a/core/test/utils/index.js b/core/test/utils/index.js index e7b00e50f71..73460c04421 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -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(); } }; /**