2018-04-06 19:19:45 +03:00
|
|
|
const _ = require('lodash'),
|
2017-12-12 00:47:46 +03:00
|
|
|
Promise = require('bluebird'),
|
|
|
|
validator = require('validator'),
|
|
|
|
ObjectId = require('bson-objectid'),
|
2016-10-06 15:27:35 +03:00
|
|
|
ghostBookshelf = require('./base'),
|
2017-12-12 00:47:46 +03:00
|
|
|
baseUtils = require('./base/utils'),
|
|
|
|
common = require('../lib/common'),
|
2017-12-14 15:52:20 +03:00
|
|
|
security = require('../lib/security'),
|
2017-12-14 22:46:53 +03:00
|
|
|
imageLib = require('../lib/image'),
|
2017-12-14 00:20:02 +03:00
|
|
|
pipeline = require('../lib/promise/pipeline'),
|
2017-12-14 22:46:53 +03:00
|
|
|
validation = require('../data/validation'),
|
2018-10-06 03:25:46 +03:00
|
|
|
permissions = require('../services/permissions'),
|
2017-12-12 00:47:46 +03:00
|
|
|
activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'],
|
🐛 be able to serve locked users (#8711)
closes #8645, closes #8710
- locked users were once part of the category "active users", but were moved to the inactive category
-> we have added a protection of not being able to edit yourself when you are either suspended or locked
- but they are not really active users, they are restricted, because they have no access to the admin panel
- support three categories: active, inactive, restricted
* - revert restricted states
- instead, update permission layer: fallback to `all` by default, because you are able to serve any user status
- add more tests
- ATTENTION: there is a behaviour change, that a blog owner's author page can be served before setting up the blog, see conversation on slack
-> LTS serves 404
-> 1.0 would serve 200
2017-07-20 14:45:13 +03:00
|
|
|
/**
|
|
|
|
* inactive: owner user before blog setup, suspended users
|
|
|
|
* locked user: imported users, they get a random passport
|
|
|
|
*/
|
2017-12-12 00:47:46 +03:00
|
|
|
inactiveStates = ['inactive', 'locked'],
|
2018-04-06 19:19:45 +03:00
|
|
|
allStates = activeStates.concat(inactiveStates);
|
|
|
|
|
|
|
|
let User, Users;
|
2013-06-25 15:43:15 +04:00
|
|
|
|
2013-09-23 02:20:08 +04:00
|
|
|
User = ghostBookshelf.Model.extend({
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
tableName: 'users',
|
|
|
|
|
2016-10-14 20:24:38 +03:00
|
|
|
defaults: function defaults() {
|
2018-02-16 02:49:15 +03:00
|
|
|
return {
|
2018-06-26 17:00:54 +03:00
|
|
|
password: security.identifier.uid(50),
|
|
|
|
visibility: 'public',
|
|
|
|
status: 'active'
|
2018-02-16 02:49:15 +03:00
|
|
|
};
|
2016-10-14 20:24:38 +03:00
|
|
|
},
|
|
|
|
|
2017-07-21 11:58:58 +03:00
|
|
|
emitChange: function emitChange(event, options) {
|
2018-04-06 19:19:45 +03:00
|
|
|
const eventToTrigger = 'user' + '.' + event;
|
|
|
|
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
2015-03-24 23:23:23 +03:00
|
|
|
},
|
|
|
|
|
2019-01-21 14:36:13 +03:00
|
|
|
/**
|
|
|
|
* @TODO:
|
|
|
|
*
|
|
|
|
* The user model does not use bookshelf-relations yet.
|
|
|
|
* Therefor we have to remove the relations manually.
|
|
|
|
*/
|
|
|
|
onDestroying(model, options) {
|
2019-02-07 12:59:37 +03:00
|
|
|
ghostBookshelf.Model.prototype.onDestroying.apply(this, arguments);
|
|
|
|
|
2019-01-21 14:36:13 +03:00
|
|
|
return (options.transacting || ghostBookshelf.knex)('roles_users')
|
|
|
|
.where('user_id', model.id)
|
|
|
|
.del();
|
|
|
|
},
|
|
|
|
|
2018-04-06 19:19:45 +03:00
|
|
|
onDestroyed: function onDestroyed(model, options) {
|
2019-02-07 12:59:37 +03:00
|
|
|
ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (_.includes(activeStates, model.previous('status'))) {
|
2017-07-21 11:58:58 +03:00
|
|
|
model.emitChange('deactivated', options);
|
2016-10-14 15:37:01 +03:00
|
|
|
}
|
2014-10-28 03:41:18 +03:00
|
|
|
|
2018-04-06 19:19:45 +03:00
|
|
|
model.emitChange('deleted', options);
|
2016-10-14 15:37:01 +03:00
|
|
|
},
|
2015-03-24 23:23:23 +03:00
|
|
|
|
2018-04-06 19:19:45 +03:00
|
|
|
onCreated: function onCreated(model, attrs, options) {
|
2019-02-07 12:59:37 +03:00
|
|
|
ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
|
|
|
|
|
2018-04-06 19:19:45 +03:00
|
|
|
model.emitChange('added', options);
|
2016-10-14 15:37:01 +03:00
|
|
|
|
|
|
|
// active is the default state, so if status isn't provided, this will be an active user
|
|
|
|
if (!model.get('status') || _.includes(activeStates, model.get('status'))) {
|
2018-04-06 19:19:45 +03:00
|
|
|
model.emitChange('activated', options);
|
2016-10-14 15:37:01 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-21 11:58:58 +03:00
|
|
|
onUpdated: function onUpdated(model, response, options) {
|
2019-02-07 12:59:37 +03:00
|
|
|
ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
|
|
|
|
|
2019-01-21 23:53:11 +03:00
|
|
|
model.statusChanging = model.get('status') !== model.previous('status');
|
2016-10-14 15:37:01 +03:00
|
|
|
model.isActive = _.includes(activeStates, model.get('status'));
|
2015-03-24 23:23:23 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (model.statusChanging) {
|
2017-07-21 11:58:58 +03:00
|
|
|
model.emitChange(model.isActive ? 'activated' : 'deactivated', options);
|
2016-10-14 15:37:01 +03:00
|
|
|
} else {
|
|
|
|
if (model.isActive) {
|
2018-04-06 19:19:45 +03:00
|
|
|
model.emitChange('activated.edited', options);
|
2015-03-24 23:23:23 +03:00
|
|
|
}
|
2016-10-14 15:37:01 +03:00
|
|
|
}
|
2015-03-24 23:23:23 +03:00
|
|
|
|
2018-04-06 19:19:45 +03:00
|
|
|
model.emitChange('edited', options);
|
2014-10-28 03:41:18 +03:00
|
|
|
},
|
|
|
|
|
2017-03-13 15:03:26 +03:00
|
|
|
isActive: function isActive() {
|
🐛 be able to serve locked users (#8711)
closes #8645, closes #8710
- locked users were once part of the category "active users", but were moved to the inactive category
-> we have added a protection of not being able to edit yourself when you are either suspended or locked
- but they are not really active users, they are restricted, because they have no access to the admin panel
- support three categories: active, inactive, restricted
* - revert restricted states
- instead, update permission layer: fallback to `all` by default, because you are able to serve any user status
- add more tests
- ATTENTION: there is a behaviour change, that a blog owner's author page can be served before setting up the blog, see conversation on slack
-> LTS serves 404
-> 1.0 would serve 200
2017-07-20 14:45:13 +03:00
|
|
|
return activeStates.indexOf(this.get('status')) !== -1;
|
2017-03-13 15:03:26 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
isLocked: function isLocked() {
|
|
|
|
return this.get('status') === 'locked';
|
|
|
|
},
|
|
|
|
|
|
|
|
isInactive: function isInactive() {
|
|
|
|
return this.get('status') === 'inactive';
|
|
|
|
},
|
|
|
|
|
2016-10-14 17:37:40 +03:00
|
|
|
/**
|
|
|
|
* Lookup Gravatar if email changes to update image url
|
|
|
|
* Generating a slug requires a db call to look for conflicting slugs
|
|
|
|
*/
|
2016-10-14 15:37:01 +03:00
|
|
|
onSaving: function onSaving(newPage, attr, options) {
|
2016-10-14 17:37:40 +03:00
|
|
|
var self = this,
|
2017-10-26 13:01:24 +03:00
|
|
|
tasks = [],
|
|
|
|
passwordValidation = {};
|
2013-08-25 14:49:31 +04:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
2013-09-14 23:01:46 +04:00
|
|
|
|
2018-02-16 02:49:15 +03:00
|
|
|
/**
|
|
|
|
* Bookshelf call order:
|
|
|
|
* - onSaving
|
|
|
|
* - onValidate (validates the model against the schema)
|
|
|
|
*
|
|
|
|
* Before we can generate a slug, we have to ensure that the name is not blank.
|
|
|
|
*/
|
|
|
|
if (!this.get('name')) {
|
|
|
|
throw new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('notices.data.validation.index.valueCannotBeBlank', {
|
|
|
|
tableName: this.tableName,
|
|
|
|
columnKey: 'name'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-08-03 11:59:05 +03:00
|
|
|
// If the user's email is set & has changed & we are not importing
|
|
|
|
if (self.hasChanged('email') && self.get('email') && !options.importing) {
|
2016-10-14 17:37:40 +03:00
|
|
|
tasks.gravatar = (function lookUpGravatar() {
|
2017-12-14 22:46:53 +03:00
|
|
|
return imageLib.gravatar.lookup({
|
2016-10-14 17:37:40 +03:00
|
|
|
email: self.get('email')
|
|
|
|
}).then(function (response) {
|
|
|
|
if (response && response.image) {
|
2017-04-24 20:21:47 +03:00
|
|
|
self.set('profile_image', response.image);
|
2016-10-14 17:37:40 +03:00
|
|
|
}
|
2013-09-14 23:01:46 +04:00
|
|
|
});
|
2016-10-14 17:37:40 +03:00
|
|
|
})();
|
2013-09-14 23:01:46 +04:00
|
|
|
}
|
2016-10-14 17:37:40 +03:00
|
|
|
|
|
|
|
if (this.hasChanged('slug') || !this.get('slug')) {
|
|
|
|
tasks.slug = (function generateSlug() {
|
|
|
|
return ghostBookshelf.Model.generateSlug(
|
|
|
|
User,
|
|
|
|
self.get('slug') || self.get('name'),
|
|
|
|
{
|
|
|
|
status: 'all',
|
|
|
|
transacting: options.transacting,
|
|
|
|
shortSlug: !self.get('slug')
|
|
|
|
})
|
|
|
|
.then(function then(slug) {
|
|
|
|
self.set({slug: slug});
|
|
|
|
});
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
2016-10-14 20:24:38 +03:00
|
|
|
/**
|
|
|
|
* CASE: add model, hash password
|
|
|
|
* CASE: update model, hash password
|
|
|
|
*
|
|
|
|
* Important:
|
|
|
|
* - Password hashing happens when we import a database
|
|
|
|
* - we do some pre-validation checks, because onValidate is called AFTER onSaving
|
2017-10-19 12:43:52 +03:00
|
|
|
* - when importing, we set the password to a random uid and don't validate, just hash it and lock the user
|
|
|
|
* - when importing with `importPersistUser` we check if the password is a bcrypt hash already and fall back to
|
|
|
|
* normal behaviour if not (set random password, lock user, and hash password)
|
|
|
|
* - no validations should run, when importing
|
2016-10-14 20:24:38 +03:00
|
|
|
*/
|
2018-02-15 22:15:43 +03:00
|
|
|
if (self.hasChanged('password')) {
|
2016-10-14 20:24:38 +03:00
|
|
|
this.set('password', String(this.get('password')));
|
|
|
|
|
2017-11-07 11:09:57 +03:00
|
|
|
// CASE: import with `importPersistUser` should always be an bcrypt password already,
|
|
|
|
// and won't re-hash or overwrite it.
|
|
|
|
// In case the password is not bcrypt hashed we fall back to the standard behaviour.
|
|
|
|
if (options.importPersistUser && this.get('password').match(/^\$2[ayb]\$.{56}$/i)) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-19 12:43:52 +03:00
|
|
|
|
2017-11-07 11:09:57 +03:00
|
|
|
if (options.importing) {
|
2017-10-19 12:43:52 +03:00
|
|
|
// always set password to a random uid when importing
|
2017-12-14 15:52:20 +03:00
|
|
|
this.set('password', security.identifier.uid(50));
|
2017-10-19 12:43:52 +03:00
|
|
|
|
|
|
|
// lock users so they have to follow the password reset flow
|
|
|
|
if (this.get('status') !== 'inactive') {
|
|
|
|
this.set('status', 'locked');
|
|
|
|
}
|
2017-10-26 13:01:24 +03:00
|
|
|
} else {
|
|
|
|
// CASE: we're not importing data, run the validations
|
|
|
|
passwordValidation = validation.validatePassword(this.get('password'), this.get('email'));
|
2016-10-14 20:24:38 +03:00
|
|
|
|
2017-10-26 13:01:24 +03:00
|
|
|
if (!passwordValidation.isValid) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: passwordValidation.message
|
|
|
|
}));
|
2017-10-26 13:01:24 +03:00
|
|
|
}
|
2017-08-22 13:15:40 +03:00
|
|
|
}
|
|
|
|
|
2016-10-14 20:24:38 +03:00
|
|
|
tasks.hashPassword = (function hashPassword() {
|
2018-02-15 23:13:04 +03:00
|
|
|
return security.password.hash(self.get('password'))
|
2016-10-14 20:24:38 +03:00
|
|
|
.then(function (hash) {
|
|
|
|
self.set('password', hash);
|
|
|
|
});
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
|
2016-10-14 17:37:40 +03:00
|
|
|
return Promise.props(tasks);
|
2013-10-07 21:02:57 +04:00
|
|
|
},
|
|
|
|
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
toJSON: function toJSON(unfilteredOptions) {
|
|
|
|
var options = User.filterOptions(unfilteredOptions, 'toJSON'),
|
|
|
|
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
2017-09-28 16:00:52 +03:00
|
|
|
|
2014-05-06 14:14:58 +04:00
|
|
|
// remove password hash for security reasons
|
|
|
|
delete attrs.password;
|
2016-10-03 17:11:43 +03:00
|
|
|
delete attrs.ghost_auth_access_token;
|
2014-05-06 05:45:08 +04:00
|
|
|
|
2017-09-28 16:00:52 +03:00
|
|
|
// NOTE: We don't expose the email address for for external, app and public context.
|
|
|
|
// @TODO: Why? External+Public is actually the same context? Was also mentioned here https://github.com/TryGhost/Ghost/issues/9043
|
2019-03-06 12:17:41 +03:00
|
|
|
// @TODO: move to api serialization when we drop v0.1
|
|
|
|
if (!options || !options.context || (!options.context.user && !options.context.internal && (!options.context.api_key || options.context.api_key.type === 'content'))) {
|
2015-04-18 00:27:04 +03:00
|
|
|
delete attrs.email;
|
|
|
|
}
|
|
|
|
|
2019-01-04 14:21:21 +03:00
|
|
|
// @TODO remove this when we remove v0.1 API as its handled in serialization for v2
|
2017-09-28 16:00:52 +03:00
|
|
|
// We don't expose these fields when fetching data via the public API.
|
|
|
|
if (options && options.context && options.context.public) {
|
|
|
|
delete attrs.created_at;
|
|
|
|
delete attrs.created_by;
|
|
|
|
delete attrs.updated_at;
|
|
|
|
delete attrs.updated_by;
|
|
|
|
delete attrs.last_seen;
|
|
|
|
delete attrs.status;
|
|
|
|
delete attrs.ghost_auth_id;
|
|
|
|
}
|
|
|
|
|
2014-05-06 14:14:58 +04:00
|
|
|
return attrs;
|
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
format: function format(options) {
|
2014-11-17 07:35:32 +03:00
|
|
|
if (!_.isEmpty(options.website) &&
|
|
|
|
!validator.isURL(options.website, {
|
2017-11-01 16:44:54 +03:00
|
|
|
require_protocol: true,
|
|
|
|
protocols: ['http', 'https']
|
|
|
|
})) {
|
2014-11-17 07:35:32 +03:00
|
|
|
options.website = 'http://' + options.website;
|
|
|
|
}
|
2015-03-25 07:11:25 +03:00
|
|
|
return ghostBookshelf.Model.prototype.format.call(this, options);
|
2014-11-17 07:35:32 +03:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
posts: function posts() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.hasMany('Posts', 'created_by');
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
2018-09-27 15:31:39 +03:00
|
|
|
sessions: function sessions() {
|
|
|
|
return this.hasMany('Sessions');
|
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
roles: function roles() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.belongsToMany('Role');
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
permissions: function permissions() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.belongsToMany('Permission');
|
2014-07-24 13:46:05 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
hasRole: function hasRole(roleName) {
|
2014-07-24 13:46:05 +04:00
|
|
|
var roles = this.related('roles');
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
return roles.some(function getRole(role) {
|
2014-07-24 13:46:05 +04:00
|
|
|
return role.get('name') === roleName;
|
|
|
|
});
|
2015-11-11 20:52:44 +03:00
|
|
|
},
|
2016-04-14 18:54:49 +03:00
|
|
|
|
2018-11-13 14:27:10 +03:00
|
|
|
updateLastSeen: function updateLastSeen() {
|
|
|
|
this.set({last_seen: new Date()});
|
|
|
|
return this.save();
|
|
|
|
},
|
|
|
|
|
2018-01-25 19:54:28 +03:00
|
|
|
enforcedFilters: function enforcedFilters(options) {
|
|
|
|
if (options.context && options.context.internal) {
|
2016-04-14 18:54:49 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-01-25 19:54:28 +03:00
|
|
|
return options.context && options.context.public ? 'status:[' + allStates.join(',') + ']' : null;
|
2015-11-11 20:52:44 +03:00
|
|
|
},
|
2016-04-14 18:54:49 +03:00
|
|
|
|
2018-01-25 19:54:28 +03:00
|
|
|
defaultFilters: function defaultFilters(options) {
|
|
|
|
if (options.context && options.context.internal) {
|
2016-10-12 18:18:57 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-01-25 19:54:28 +03:00
|
|
|
return options.context && options.context.public ? null : 'status:[' + allStates.join(',') + ']';
|
2015-06-17 16:55:39 +03:00
|
|
|
},
|
|
|
|
|
2015-11-11 22:31:52 +03:00
|
|
|
/**
|
2018-11-15 17:53:24 +03:00
|
|
|
* You can pass an extra `status=VALUES` field.
|
|
|
|
* Long-Term: We should deprecate these short cuts and force users to use the filter param.
|
2015-11-11 22:31:52 +03:00
|
|
|
*/
|
2018-11-15 17:53:24 +03:00
|
|
|
extraFilters: function extraFilters(options) {
|
2015-11-11 22:31:52 +03:00
|
|
|
if (!options.status) {
|
2018-11-15 17:53:24 +03:00
|
|
|
return null;
|
2015-11-11 22:31:52 +03:00
|
|
|
}
|
|
|
|
|
2018-11-15 17:53:24 +03:00
|
|
|
let filter = null;
|
2015-11-11 22:31:52 +03:00
|
|
|
|
2018-11-15 17:53:24 +03:00
|
|
|
// CASE: Check if the incoming status value is valid, otherwise fallback to "active"
|
2015-11-11 22:31:52 +03:00
|
|
|
if (options.status !== 'all') {
|
|
|
|
options.status = allStates.indexOf(options.status) > -1 ? options.status : 'active';
|
2015-06-17 16:55:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (options.status === 'active') {
|
2018-11-15 17:53:24 +03:00
|
|
|
filter = `status:[${activeStates}]`;
|
2015-11-11 22:31:52 +03:00
|
|
|
} else if (options.status === 'all') {
|
2018-11-15 17:53:24 +03:00
|
|
|
filter = `status:[${allStates}]`;
|
2015-11-11 22:31:52 +03:00
|
|
|
} else {
|
2018-11-15 17:53:24 +03:00
|
|
|
filter = `status:${options.status}`;
|
2015-06-17 16:55:39 +03:00
|
|
|
}
|
|
|
|
|
2015-11-11 22:31:52 +03:00
|
|
|
delete options.status;
|
|
|
|
|
2018-11-15 17:53:24 +03:00
|
|
|
return filter;
|
|
|
|
}
|
|
|
|
}, {
|
|
|
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
|
|
return {
|
|
|
|
last_seen: 'DESC',
|
|
|
|
name: 'ASC',
|
|
|
|
created_at: 'DESC'
|
|
|
|
};
|
2015-06-17 16:55:39 +03:00
|
|
|
},
|
|
|
|
|
2014-05-06 05:45:08 +04:00
|
|
|
/**
|
2017-12-12 00:47:46 +03:00
|
|
|
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
|
|
|
|
* @param {String} methodName The name of the method to check valid options for.
|
|
|
|
* @return {Array} Keys allowed in the `options` hash of the model's method.
|
|
|
|
*/
|
2017-09-26 17:43:21 +03:00
|
|
|
permittedOptions: function permittedOptions(methodName, options) {
|
2018-09-20 17:23:39 +03:00
|
|
|
var permittedOptionsToReturn = ghostBookshelf.Model.permittedOptions.call(this, methodName),
|
2014-05-06 05:45:08 +04:00
|
|
|
|
|
|
|
// whitelists for the `options` hash argument on methods, by method name.
|
|
|
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
|
|
validOptions = {
|
2014-08-05 22:11:17 +04:00
|
|
|
findOne: ['withRelated', 'status'],
|
2014-07-15 15:03:12 +04:00
|
|
|
setup: ['id'],
|
2018-09-20 19:08:24 +03:00
|
|
|
edit: ['withRelated', 'importPersistUser'],
|
2017-08-22 13:15:40 +03:00
|
|
|
add: ['importPersistUser'],
|
2019-01-30 01:29:06 +03:00
|
|
|
findPage: ['status'],
|
2016-04-14 18:54:49 +03:00
|
|
|
findAll: ['filter']
|
2014-05-06 05:45:08 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
if (validOptions[methodName]) {
|
2017-09-26 17:43:21 +03:00
|
|
|
permittedOptionsToReturn = permittedOptionsToReturn.concat(validOptions[methodName]);
|
2014-05-06 05:45:08 +04:00
|
|
|
}
|
|
|
|
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
// CASE: The `withRelated` parameter is allowed when using the public API, but not the `roles` value.
|
2017-09-26 17:43:21 +03:00
|
|
|
// Otherwise we expose too much information.
|
2018-11-07 17:29:37 +03:00
|
|
|
// @TODO: the target controller should define the allowed includes, but not the model layer O_O (https://github.com/TryGhost/Ghost/issues/10106)
|
2017-09-26 17:43:21 +03:00
|
|
|
if (options && options.context && options.context.public) {
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
if (options.withRelated && options.withRelated.indexOf('roles') !== -1) {
|
|
|
|
options.withRelated.splice(options.withRelated.indexOf('roles'), 1);
|
2017-09-26 17:43:21 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return permittedOptionsToReturn;
|
2014-05-06 05:45:08 +04:00
|
|
|
},
|
2013-06-25 15:43:15 +04:00
|
|
|
|
2014-07-08 20:00:59 +04:00
|
|
|
/**
|
|
|
|
* ### Find One
|
2016-10-12 18:18:57 +03:00
|
|
|
*
|
|
|
|
* We have to clone the data, because we remove values from this object.
|
|
|
|
* This is not expected from outside!
|
|
|
|
*
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
* @TODO: use base class
|
|
|
|
*
|
2014-07-08 20:00:59 +04:00
|
|
|
* @extends ghostBookshelf.Model.findOne to include roles
|
|
|
|
* **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
|
|
|
|
*/
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
findOne: function findOne(dataToClone, unfilteredOptions) {
|
|
|
|
var options = this.filterOptions(unfilteredOptions, 'findOne'),
|
|
|
|
query,
|
2015-01-20 20:40:25 +03:00
|
|
|
status,
|
2016-10-12 18:18:57 +03:00
|
|
|
data = _.cloneDeep(dataToClone),
|
2015-01-20 20:40:25 +03:00
|
|
|
lookupRole = data.role;
|
|
|
|
|
2019-03-05 12:19:24 +03:00
|
|
|
// Ensure only valid fields/columns are added to query
|
|
|
|
if (options.columns) {
|
|
|
|
options.columns = _.intersection(options.columns, this.prototype.permittedAttributes());
|
|
|
|
}
|
|
|
|
|
2015-01-20 20:40:25 +03:00
|
|
|
delete data.role;
|
2015-06-25 21:56:27 +03:00
|
|
|
data = _.defaults(data || {}, {
|
2017-03-13 15:03:26 +03:00
|
|
|
status: 'all'
|
2015-06-25 21:56:27 +03:00
|
|
|
});
|
2014-08-05 22:11:17 +04:00
|
|
|
|
|
|
|
status = data.status;
|
|
|
|
delete data.status;
|
|
|
|
|
2015-06-15 19:45:58 +03:00
|
|
|
data = this.filterData(data);
|
2014-07-08 20:00:59 +04:00
|
|
|
|
2014-07-15 15:03:12 +04:00
|
|
|
// Support finding by role
|
2015-01-20 20:40:25 +03:00
|
|
|
if (lookupRole) {
|
|
|
|
options.withRelated = _.union(options.withRelated, ['roles']);
|
2018-02-14 19:32:11 +03:00
|
|
|
query = this.forge(data);
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
|
2016-05-22 11:37:44 +03:00
|
|
|
query.query('join', 'roles_users', 'users.id', '=', 'roles_users.user_id');
|
2015-01-20 20:40:25 +03:00
|
|
|
query.query('join', 'roles', 'roles_users.role_id', '=', 'roles.id');
|
|
|
|
query.query('where', 'roles.name', '=', lookupRole);
|
|
|
|
} else {
|
2018-02-14 19:32:11 +03:00
|
|
|
query = this.forge(data);
|
2015-01-20 20:40:25 +03:00
|
|
|
}
|
2014-08-05 22:11:17 +04:00
|
|
|
|
|
|
|
if (status === 'active') {
|
|
|
|
query.query('whereIn', 'status', activeStates);
|
|
|
|
} else if (status !== 'all') {
|
2016-10-12 18:18:57 +03:00
|
|
|
query.query('where', {status: status});
|
2014-07-31 08:25:42 +04:00
|
|
|
}
|
|
|
|
|
2014-08-05 22:11:17 +04:00
|
|
|
return query.fetch(options);
|
2014-07-08 20:00:59 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Edit
|
2017-02-08 12:50:43 +03:00
|
|
|
*
|
2017-04-05 22:45:55 +03:00
|
|
|
* Note: In case of login the last_seen attribute gets updated.
|
2017-02-08 12:50:43 +03:00
|
|
|
*
|
2014-07-08 20:00:59 +04:00
|
|
|
* @extends ghostBookshelf.Model.edit to handle returning the full object
|
|
|
|
* **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
|
|
|
|
*/
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
edit: function edit(data, unfilteredOptions) {
|
|
|
|
var options = this.filterOptions(unfilteredOptions, 'edit'),
|
|
|
|
self = this,
|
2017-02-08 12:50:43 +03:00
|
|
|
ops = [];
|
2014-07-22 00:50:43 +04:00
|
|
|
|
2014-12-17 18:36:08 +03:00
|
|
|
if (data.roles && data.roles.length > 1) {
|
|
|
|
return Promise.reject(
|
2018-10-06 22:22:02 +03:00
|
|
|
new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.onlyOneRolePerUserSupported')
|
|
|
|
})
|
2014-12-17 18:36:08 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-02-13 18:36:21 +03:00
|
|
|
if (data.email) {
|
2017-02-08 12:50:43 +03:00
|
|
|
ops.push(function checkForDuplicateEmail() {
|
2017-02-23 21:04:24 +03:00
|
|
|
return self.getByEmail(data.email, options).then(function then(user) {
|
2017-02-13 18:36:21 +03:00
|
|
|
if (user && user.id !== options.id) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.userUpdateError.emailIsAlreadyInUse')
|
|
|
|
}));
|
2017-02-08 12:50:43 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2014-12-17 18:36:08 +03:00
|
|
|
|
2017-02-08 12:50:43 +03:00
|
|
|
ops.push(function update() {
|
2019-02-08 00:07:13 +03:00
|
|
|
return ghostBookshelf.Model.edit.call(self, data, options).then((user) => {
|
2017-02-08 12:50:43 +03:00
|
|
|
var roleId;
|
2014-07-22 00:50:43 +04:00
|
|
|
|
2017-02-08 12:50:43 +03:00
|
|
|
if (!data.roles) {
|
|
|
|
return user;
|
2014-12-17 18:36:08 +03:00
|
|
|
}
|
2017-02-08 12:50:43 +03:00
|
|
|
|
|
|
|
roleId = data.roles[0].id || data.roles[0];
|
|
|
|
|
2019-02-08 00:07:13 +03:00
|
|
|
return user.roles().fetch().then((roles) => {
|
2017-02-08 12:50:43 +03:00
|
|
|
// return if the role is already assigned
|
|
|
|
if (roles.models[0].id === roleId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return ghostBookshelf.model('Role').findOne({id: roleId});
|
2019-02-08 00:07:13 +03:00
|
|
|
}).then((roleToAssign) => {
|
2017-02-08 12:50:43 +03:00
|
|
|
if (roleToAssign && roleToAssign.get('name') === 'Owner') {
|
|
|
|
return Promise.reject(
|
2018-10-06 22:22:02 +03:00
|
|
|
new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.methodDoesNotSupportOwnerRole')
|
|
|
|
})
|
2017-02-08 12:50:43 +03:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// assign all other roles
|
|
|
|
return user.roles().updatePivot({role_id: roleId});
|
|
|
|
}
|
2019-02-08 00:07:13 +03:00
|
|
|
}).then(() => {
|
2017-02-08 12:50:43 +03:00
|
|
|
options.status = 'all';
|
|
|
|
return self.findOne({id: user.id}, options);
|
2019-02-08 00:07:13 +03:00
|
|
|
}).then((model) => {
|
|
|
|
model._changed = user._changed;
|
|
|
|
return model;
|
2017-02-08 12:50:43 +03:00
|
|
|
});
|
2014-12-17 18:36:08 +03:00
|
|
|
});
|
2014-07-22 00:50:43 +04:00
|
|
|
});
|
2017-02-08 12:50:43 +03:00
|
|
|
|
|
|
|
return pipeline(ops);
|
2014-07-08 20:00:59 +04:00
|
|
|
},
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
/**
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
* ## Add
|
2013-06-25 15:43:15 +04:00
|
|
|
* Naive user add
|
|
|
|
* Hashes the password provided before saving to the database.
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
*
|
2016-10-12 18:18:57 +03:00
|
|
|
* We have to clone the data, because we remove values from this object.
|
|
|
|
* This is not expected from outside!
|
|
|
|
*
|
|
|
|
* @param {object} dataToClone
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
* @param {object} unfilteredOptions
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
* @extends ghostBookshelf.Model.add to manage all aspects of user signup
|
|
|
|
* **See:** [ghostBookshelf.Model.add](base.js.html#Add)
|
2013-06-25 15:43:15 +04:00
|
|
|
*/
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
add: function add(dataToClone, unfilteredOptions) {
|
|
|
|
var options = this.filterOptions(unfilteredOptions, 'add'),
|
|
|
|
self = this,
|
2016-10-12 18:18:57 +03:00
|
|
|
data = _.cloneDeep(dataToClone),
|
2014-07-24 13:46:05 +04:00
|
|
|
userData = this.filterData(data),
|
2014-07-30 19:40:30 +04:00
|
|
|
roles;
|
2014-05-06 05:45:08 +04:00
|
|
|
|
2014-12-17 18:36:08 +03:00
|
|
|
// check for too many roles
|
|
|
|
if (data.roles && data.roles.length > 1) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.onlyOneRolePerUserSupported')
|
|
|
|
}));
|
2014-12-17 18:36:08 +03:00
|
|
|
}
|
2014-07-31 04:15:34 +04:00
|
|
|
|
2014-12-17 18:36:08 +03:00
|
|
|
function getAuthorRole() {
|
2017-05-23 19:18:13 +03:00
|
|
|
return ghostBookshelf.model('Role').findOne({name: 'Author'}, _.pick(options, 'transacting'))
|
|
|
|
.then(function then(authorRole) {
|
|
|
|
return [authorRole.get('id')];
|
|
|
|
});
|
2014-12-17 18:36:08 +03:00
|
|
|
}
|
|
|
|
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
/**
|
|
|
|
* We need this default author role because of the following Ghost feature:
|
|
|
|
* You setup your blog and you can invite people instantly, but without choosing a role.
|
|
|
|
* roles: [] -> no default role (used for owner creation, see fixtures.json)
|
|
|
|
* roles: undefined -> default role
|
|
|
|
*/
|
2017-05-23 19:18:13 +03:00
|
|
|
roles = data.roles;
|
2014-12-17 18:36:08 +03:00
|
|
|
delete data.roles;
|
|
|
|
|
2016-10-14 20:24:38 +03:00
|
|
|
return ghostBookshelf.Model.add.call(self, userData, options)
|
|
|
|
.then(function then(addedUser) {
|
|
|
|
// Assign the userData to our created user so we can pass it back
|
|
|
|
userData = addedUser;
|
2017-05-23 19:18:13 +03:00
|
|
|
})
|
|
|
|
.then(function () {
|
|
|
|
if (!roles) {
|
|
|
|
return getAuthorRole();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve(roles);
|
|
|
|
})
|
|
|
|
.then(function (_roles) {
|
|
|
|
roles = _roles;
|
|
|
|
|
|
|
|
// CASE: it is possible to add roles by name, by id or by object
|
|
|
|
if (_.isString(roles[0]) && !ObjectId.isValid(roles[0])) {
|
|
|
|
return Promise.map(roles, function (roleName) {
|
|
|
|
return ghostBookshelf.model('Role').findOne({
|
|
|
|
name: roleName
|
|
|
|
}, options);
|
|
|
|
}).then(function (roleModels) {
|
|
|
|
roles = [];
|
|
|
|
|
|
|
|
_.each(roleModels, function (roleModel) {
|
|
|
|
roles.push(roleModel.id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2016-10-14 20:24:38 +03:00
|
|
|
|
2017-05-23 19:18:13 +03:00
|
|
|
return Promise.resolve();
|
|
|
|
})
|
|
|
|
.then(function () {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
return baseUtils.attach(User, userData.id, 'roles', roles, options);
|
2017-05-23 19:18:13 +03:00
|
|
|
})
|
|
|
|
.then(function then() {
|
2016-10-14 20:24:38 +03:00
|
|
|
// find and return the added user
|
|
|
|
return self.findOne({id: userData.id, status: 'all'}, options);
|
2014-12-17 18:36:08 +03:00
|
|
|
});
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
2019-01-21 14:36:13 +03:00
|
|
|
destroy: function destroy(unfilteredOptions) {
|
|
|
|
const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
|
|
|
|
|
|
const destroyUser = () => {
|
|
|
|
return ghostBookshelf.Model.destroy.call(this, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!options.transacting) {
|
|
|
|
return ghostBookshelf.transaction((transacting) => {
|
|
|
|
options.transacting = transacting;
|
|
|
|
return destroyUser();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return destroyUser();
|
|
|
|
},
|
|
|
|
|
2016-10-14 20:24:38 +03:00
|
|
|
/**
|
|
|
|
* We override the owner!
|
|
|
|
* Owner already has a slug -> force setting a new one by setting slug to null
|
|
|
|
* @TODO: kill setup function?
|
|
|
|
*/
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
setup: function setup(data, unfilteredOptions) {
|
|
|
|
var options = this.filterOptions(unfilteredOptions, 'setup'),
|
|
|
|
self = this,
|
2017-10-26 13:01:24 +03:00
|
|
|
userData = this.filterData(data),
|
|
|
|
passwordValidation = {};
|
|
|
|
|
|
|
|
passwordValidation = validation.validatePassword(userData.password, userData.email, data.blogTitle);
|
2014-07-11 16:17:09 +04:00
|
|
|
|
2017-10-26 13:01:24 +03:00
|
|
|
if (!passwordValidation.isValid) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: passwordValidation.message
|
|
|
|
}));
|
2014-12-17 18:36:08 +03:00
|
|
|
}
|
|
|
|
|
2016-10-14 20:24:38 +03:00
|
|
|
userData.slug = null;
|
2017-11-01 16:44:54 +03:00
|
|
|
return self.edit(userData, options);
|
2014-07-10 21:29:51 +04:00
|
|
|
},
|
|
|
|
|
2017-03-02 22:50:58 +03:00
|
|
|
/**
|
|
|
|
* Right now the setup of the blog depends on the user status.
|
2017-07-31 12:37:37 +03:00
|
|
|
* Only if the owner user is `inactive`, then the blog is not setup.
|
|
|
|
* e.g. if you transfer ownership to a locked user, you blog is still setup.
|
|
|
|
*
|
|
|
|
* @TODO: Rename `inactive` status to something else, it's confusing. e.g. requires-setup
|
|
|
|
* @TODO: Depending on the user status results in https://github.com/TryGhost/Ghost/issues/8003
|
2017-03-02 22:50:58 +03:00
|
|
|
*/
|
2017-07-31 12:37:37 +03:00
|
|
|
isSetup: function isSetup(options) {
|
|
|
|
return this.getOwnerUser(options)
|
|
|
|
.then(function (owner) {
|
|
|
|
return owner.get('status') !== 'inactive';
|
2017-03-02 22:50:58 +03:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-07-31 12:37:37 +03:00
|
|
|
getOwnerUser: function getOwnerUser(options) {
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
return this.findOne({
|
|
|
|
role: 'Owner',
|
|
|
|
status: 'all'
|
|
|
|
}, options).then(function (owner) {
|
|
|
|
if (!owner) {
|
2017-12-12 00:47:46 +03:00
|
|
|
return Promise.reject(new common.errors.NotFoundError({
|
|
|
|
message: common.i18n.t('errors.models.user.ownerNotFound')
|
2017-07-31 12:37:37 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return owner;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2019-01-18 15:39:53 +03:00
|
|
|
permissible: function permissible(userModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
|
2014-04-08 17:40:33 +04:00
|
|
|
var self = this,
|
2014-05-14 05:49:07 +04:00
|
|
|
userModel = userModelOrId,
|
|
|
|
origArgs;
|
2014-04-08 17:40:33 +04:00
|
|
|
|
2014-11-27 03:28:29 +03:00
|
|
|
// If we passed in a model without its related roles, we need to fetch it again
|
|
|
|
if (_.isObject(userModelOrId) && !_.isObject(userModelOrId.related('roles'))) {
|
|
|
|
userModelOrId = userModelOrId.id;
|
|
|
|
}
|
|
|
|
// If we passed in an id instead of a model get the model first
|
2014-04-08 17:40:33 +04:00
|
|
|
if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
|
2014-05-14 05:49:07 +04:00
|
|
|
// Grab the original args without the first one
|
|
|
|
origArgs = _.toArray(arguments).slice(1);
|
2016-10-06 15:27:35 +03:00
|
|
|
|
2015-12-02 10:28:36 +03:00
|
|
|
// Get the actual user model
|
2017-12-12 00:47:46 +03:00
|
|
|
return this.findOne({
|
|
|
|
id: userModelOrId,
|
|
|
|
status: 'all'
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
}, {withRelated: ['roles']}).then(function then(foundUserModel) {
|
2017-07-18 17:14:02 +03:00
|
|
|
if (!foundUserModel) {
|
2017-12-12 00:47:46 +03:00
|
|
|
throw new common.errors.NotFoundError({
|
|
|
|
message: common.i18n.t('errors.models.user.userNotFound')
|
2017-07-18 17:14:02 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-05-14 05:49:07 +04:00
|
|
|
// Build up the original args but substitute with actual model
|
|
|
|
var newArgs = [foundUserModel].concat(origArgs);
|
|
|
|
|
2014-07-23 22:17:29 +04:00
|
|
|
return self.permissible.apply(self, newArgs);
|
2016-10-04 18:33:43 +03:00
|
|
|
});
|
2014-04-08 17:40:33 +04:00
|
|
|
}
|
|
|
|
|
2014-07-24 13:46:05 +04:00
|
|
|
if (action === 'edit') {
|
2018-02-07 12:46:22 +03:00
|
|
|
// Users with the role 'Editor', 'Author', and 'Contributor' have complex permissions when the action === 'edit'
|
2014-07-24 13:46:05 +04:00
|
|
|
// We now have all the info we need to construct the permissions
|
|
|
|
|
2018-02-07 12:46:22 +03:00
|
|
|
if (context.user === userModel.get('id')) {
|
2014-07-24 13:46:05 +04:00
|
|
|
// If this is the same user that requests the operation allow it.
|
2018-02-07 12:46:22 +03:00
|
|
|
hasUserPermission = true;
|
|
|
|
} else if (loadedPermissions.user && userModel.hasRole('Owner')) {
|
|
|
|
// Owner can only be edited by owner
|
|
|
|
hasUserPermission = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
|
|
|
|
} else if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
|
|
|
|
// If the user we are trying to edit is an Author or Contributor, allow it
|
|
|
|
hasUserPermission = userModel.hasRole('Author') || userModel.hasRole('Contributor');
|
2014-07-24 13:46:05 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (action === 'destroy') {
|
|
|
|
// Owner cannot be deleted EVER
|
2018-02-07 12:46:22 +03:00
|
|
|
if (userModel.hasRole('Owner')) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.notEnoughPermission')
|
|
|
|
}));
|
2014-07-24 13:46:05 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Users with the role 'Editor' have complex permissions when the action === 'destroy'
|
2016-06-11 21:23:27 +03:00
|
|
|
if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
|
2014-07-24 13:46:05 +04:00
|
|
|
// Alternatively, if the user we are trying to edit is an Author, allow it
|
2018-02-07 12:46:22 +03:00
|
|
|
hasUserPermission = context.user === userModel.get('id') || userModel.hasRole('Author') || userModel.hasRole('Contributor');
|
2014-07-24 13:46:05 +04:00
|
|
|
}
|
2014-05-14 05:49:07 +04:00
|
|
|
}
|
|
|
|
|
2018-10-06 03:25:46 +03:00
|
|
|
// CASE: can't edit my own status to inactive or locked
|
|
|
|
if (action === 'edit' && userModel.id === context.user) {
|
|
|
|
if (User.inactiveStates.indexOf(unsafeAttrs.status) !== -1) {
|
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.api.users.cannotChangeStatus')
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// CASE: i want to edit roles
|
|
|
|
if (action === 'edit' && unsafeAttrs.roles && unsafeAttrs.roles[0]) {
|
|
|
|
let role = unsafeAttrs.roles[0];
|
|
|
|
let roleId = role.id || role;
|
|
|
|
let editedUserId = userModel.id;
|
2018-10-06 22:24:02 +03:00
|
|
|
// @NOTE: role id of logged in user
|
2018-10-06 12:07:37 +03:00
|
|
|
let contextRoleId = loadedPermissions.user.roles[0].id;
|
2018-10-06 03:25:46 +03:00
|
|
|
|
|
|
|
if (roleId !== contextRoleId && editedUserId === context.user) {
|
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.api.users.cannotChangeOwnRole')
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
return User.getOwnerUser()
|
|
|
|
.then((owner) => {
|
2018-10-06 22:24:02 +03:00
|
|
|
// CASE: owner can assign role to any user
|
2018-10-06 12:07:37 +03:00
|
|
|
if (context.user === owner.id) {
|
2019-01-18 15:39:53 +03:00
|
|
|
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
2018-10-06 03:25:46 +03:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.notEnoughPermission')
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2018-10-06 22:24:02 +03:00
|
|
|
// CASE: You try to change the role of the owner user
|
2018-10-06 03:25:46 +03:00
|
|
|
if (editedUserId === owner.id) {
|
|
|
|
if (owner.related('roles').at(0).id !== roleId) {
|
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.api.users.cannotChangeOwnersRole')
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
} else if (roleId !== contextRoleId) {
|
2018-10-06 22:24:02 +03:00
|
|
|
// CASE: you are trying to change a role, but you are not owner
|
|
|
|
// @NOTE: your role is not the same than the role you try to change (!)
|
|
|
|
// e.g. admin can assign admin role to a user, but not owner
|
2018-10-06 03:25:46 +03:00
|
|
|
return permissions.canThis(context).assign.role(role)
|
|
|
|
.then(() => {
|
2019-01-18 15:39:53 +03:00
|
|
|
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
2018-10-06 03:25:46 +03:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.notEnoughPermission')
|
|
|
|
}));
|
|
|
|
});
|
2018-10-06 12:07:37 +03:00
|
|
|
}
|
2018-10-06 03:25:46 +03:00
|
|
|
|
2019-01-18 15:39:53 +03:00
|
|
|
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
2018-10-06 12:07:37 +03:00
|
|
|
return Promise.resolve();
|
2018-10-06 03:25:46 +03:00
|
|
|
}
|
2018-10-06 12:07:37 +03:00
|
|
|
|
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.notEnoughPermission')
|
|
|
|
}));
|
2018-10-06 03:25:46 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-01-18 15:39:53 +03:00
|
|
|
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.resolve();
|
2014-04-08 17:40:33 +04:00
|
|
|
}
|
2014-05-14 05:49:07 +04:00
|
|
|
|
2018-10-06 03:25:46 +03:00
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.notEnoughPermission')
|
|
|
|
}));
|
2014-04-08 17:40:33 +04:00
|
|
|
},
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
// Finds the user by email, and checks the password
|
2016-10-14 20:46:22 +03:00
|
|
|
// @TODO: shorten this function and rename...
|
2015-06-14 18:58:49 +03:00
|
|
|
check: function check(object) {
|
2016-11-08 14:33:19 +03:00
|
|
|
var self = this;
|
2016-10-14 20:46:22 +03:00
|
|
|
|
2018-02-16 02:49:15 +03:00
|
|
|
return this.getByEmail(object.email)
|
2018-11-13 14:27:10 +03:00
|
|
|
.then((user) => {
|
2018-02-16 02:49:15 +03:00
|
|
|
if (!user) {
|
|
|
|
throw new common.errors.NotFoundError({
|
|
|
|
message: common.i18n.t('errors.models.user.noUserWithEnteredEmailAddr')
|
|
|
|
});
|
|
|
|
}
|
2016-09-21 17:48:14 +03:00
|
|
|
|
2018-02-16 02:49:15 +03:00
|
|
|
if (user.isLocked()) {
|
|
|
|
throw new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.accountLocked')
|
|
|
|
});
|
|
|
|
}
|
2017-03-13 15:03:26 +03:00
|
|
|
|
2018-02-16 02:49:15 +03:00
|
|
|
if (user.isInactive()) {
|
|
|
|
throw new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.accountSuspended')
|
|
|
|
});
|
|
|
|
}
|
2016-10-14 20:46:22 +03:00
|
|
|
|
2018-02-16 02:49:15 +03:00
|
|
|
return self.isPasswordCorrect({plainPassword: object.password, hashedPassword: user.get('password')})
|
2018-11-13 14:27:10 +03:00
|
|
|
.then(() => {
|
|
|
|
return user.updateLastSeen();
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
user.set({status: 'active'});
|
2018-02-16 02:49:15 +03:00
|
|
|
return user.save();
|
|
|
|
});
|
|
|
|
})
|
2018-11-13 14:27:10 +03:00
|
|
|
.catch((err) => {
|
2018-02-16 02:49:15 +03:00
|
|
|
if (err.message === 'NotFound' || err.message === 'EmptyResponse') {
|
|
|
|
throw new common.errors.NotFoundError({
|
|
|
|
message: common.i18n.t('errors.models.user.noUserWithEnteredEmailAddr')
|
|
|
|
});
|
|
|
|
}
|
2014-01-15 02:47:17 +04:00
|
|
|
|
2018-02-16 02:49:15 +03:00
|
|
|
throw err;
|
|
|
|
});
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
|
|
|
|
2016-10-14 20:46:22 +03:00
|
|
|
isPasswordCorrect: function isPasswordCorrect(object) {
|
|
|
|
var plainPassword = object.plainPassword,
|
|
|
|
hashedPassword = object.hashedPassword;
|
|
|
|
|
|
|
|
if (!plainPassword || !hashedPassword) {
|
2017-12-12 00:47:46 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.passwordRequiredForOperation')
|
2016-10-14 20:46:22 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2018-02-15 23:13:04 +03:00
|
|
|
return security.password.compare(plainPassword, hashedPassword)
|
2016-10-14 20:46:22 +03:00
|
|
|
.then(function (matched) {
|
|
|
|
if (matched) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-12-12 00:47:46 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
context: common.i18n.t('errors.models.user.incorrectPassword'),
|
|
|
|
message: common.i18n.t('errors.models.user.incorrectPassword'),
|
|
|
|
help: common.i18n.t('errors.models.user.userUpdateError.help'),
|
2016-10-14 20:46:22 +03:00
|
|
|
code: 'PASSWORD_INCORRECT'
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-08-06 03:49:06 +04:00
|
|
|
/**
|
|
|
|
* Naive change password method
|
2015-07-07 16:32:50 +03:00
|
|
|
* @param {Object} object
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
* @param {Object} unfilteredOptions
|
2013-08-06 03:49:06 +04:00
|
|
|
*/
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
changePassword: function changePassword(object, unfilteredOptions) {
|
|
|
|
var options = this.filterOptions(unfilteredOptions, 'changePassword'),
|
|
|
|
self = this,
|
2015-07-07 16:32:50 +03:00
|
|
|
newPassword = object.newPassword,
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
userId = object.user_id,
|
2015-07-07 16:32:50 +03:00
|
|
|
oldPassword = object.oldPassword,
|
2016-10-21 12:19:09 +03:00
|
|
|
isLoggedInUser = userId === options.context.user,
|
2014-12-17 18:36:08 +03:00
|
|
|
user;
|
2013-09-01 02:20:12 +04:00
|
|
|
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
options.require = true;
|
|
|
|
|
|
|
|
return self.forge({id: userId}).fetch(options)
|
2016-10-14 20:46:22 +03:00
|
|
|
.then(function then(_user) {
|
|
|
|
user = _user;
|
2016-10-14 20:24:38 +03:00
|
|
|
|
2016-10-14 20:46:22 +03:00
|
|
|
if (isLoggedInUser) {
|
|
|
|
return self.isPasswordCorrect({
|
|
|
|
plainPassword: oldPassword,
|
|
|
|
hashedPassword: user.get('password')
|
|
|
|
});
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(function then() {
|
|
|
|
return user.save({password: newPassword});
|
|
|
|
});
|
2013-09-01 02:20:12 +04:00
|
|
|
},
|
|
|
|
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
transferOwnership: function transferOwnership(object, unfilteredOptions) {
|
2019-04-16 12:30:29 +03:00
|
|
|
const options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'transferOwnership');
|
|
|
|
let ownerRole;
|
|
|
|
let contextUser;
|
2014-12-17 18:36:08 +03:00
|
|
|
|
2017-11-01 16:44:54 +03:00
|
|
|
return Promise.join(
|
|
|
|
ghostBookshelf.model('Role').findOne({name: 'Owner'}),
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
User.findOne({id: options.context.user}, {withRelated: ['roles']})
|
2017-11-01 16:44:54 +03:00
|
|
|
)
|
2019-04-16 12:30:29 +03:00
|
|
|
.then((results) => {
|
2017-11-01 16:44:54 +03:00
|
|
|
ownerRole = results[0];
|
|
|
|
contextUser = results[1];
|
|
|
|
|
|
|
|
// check if user has the owner role
|
2019-04-16 12:30:29 +03:00
|
|
|
const currentRoles = contextUser.toJSON(options).roles;
|
2017-11-01 16:44:54 +03:00
|
|
|
if (!_.some(currentRoles, {id: ownerRole.id})) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.NoPermissionError({
|
|
|
|
message: common.i18n.t('errors.models.user.onlyOwnerCanTransferOwnerRole')
|
|
|
|
}));
|
2017-11-01 16:44:54 +03:00
|
|
|
}
|
2014-12-17 18:36:08 +03:00
|
|
|
|
2017-11-01 16:44:54 +03:00
|
|
|
return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
User.findOne({id: object.id}, {withRelated: ['roles']}));
|
2017-11-01 16:44:54 +03:00
|
|
|
})
|
2019-04-16 12:30:29 +03:00
|
|
|
.then((results) => {
|
2018-10-06 22:50:31 +03:00
|
|
|
const adminRole = results[0];
|
|
|
|
const user = results[1];
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
return Promise.reject(new common.errors.NotFoundError({
|
|
|
|
message: common.i18n.t('errors.models.user.userNotFound')
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2019-04-16 12:30:29 +03:00
|
|
|
const {roles: currentRoles, status} = user.toJSON(options);
|
2014-12-17 18:36:08 +03:00
|
|
|
|
2017-11-01 16:44:54 +03:00
|
|
|
if (!_.some(currentRoles, {id: adminRole.id})) {
|
2018-10-06 22:22:02 +03:00
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.onlyAdmCanBeAssignedOwnerRole')
|
|
|
|
}));
|
2017-11-01 16:44:54 +03:00
|
|
|
}
|
2014-07-30 19:40:30 +04:00
|
|
|
|
2019-04-16 12:30:29 +03:00
|
|
|
if (status !== 'active') {
|
|
|
|
return Promise.reject(new common.errors.ValidationError({
|
|
|
|
message: common.i18n.t('errors.models.user.onlyActiveAdmCanBeAssignedOwnerRole')
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2017-11-01 16:44:54 +03:00
|
|
|
// convert owner to admin
|
|
|
|
return Promise.join(contextUser.roles().updatePivot({role_id: adminRole.id}),
|
|
|
|
user.roles().updatePivot({role_id: ownerRole.id}),
|
|
|
|
user.id);
|
|
|
|
})
|
2019-04-16 12:30:29 +03:00
|
|
|
.then((results) => {
|
2017-11-01 16:44:54 +03:00
|
|
|
return Users.forge()
|
|
|
|
.query('whereIn', 'id', [contextUser.id, results[2]])
|
|
|
|
.fetch({withRelated: ['roles']});
|
|
|
|
});
|
2014-07-24 13:46:05 +04:00
|
|
|
},
|
|
|
|
|
2014-01-15 02:47:17 +04:00
|
|
|
// Get the user by email address, enforces case insensitivity rejects if the user is not found
|
|
|
|
// When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
|
|
|
|
// joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
getByEmail: function getByEmail(email, unfilteredOptions) {
|
|
|
|
var options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'getByEmail');
|
|
|
|
|
2014-01-15 02:47:17 +04:00
|
|
|
// We fetch all users and process them in JS as there is no easy way to make this query across all DBs
|
|
|
|
// Although they all support `lower()`, sqlite can't case transform unicode characters
|
|
|
|
// This is somewhat mute, as validator.isEmail() also doesn't support unicode, but this is much easier / more
|
|
|
|
// likely to be fixed in the near future.
|
2014-07-31 04:15:34 +04:00
|
|
|
options.require = true;
|
|
|
|
|
Sorted out the mixed usages of `include` and `withRelated` (#9425)
no issue
- this commit cleans up the usages of `include` and `withRelated`.
### API layer (`include`)
- as request parameter e.g. `?include=roles,tags`
- as theme API parameter e.g. `{{get .... include="author"}}`
- as internal API access e.g. `api.posts.browse({include: 'author,tags'})`
- the `include` notation is more readable than `withRelated`
- and it allows us to use a different easier format (comma separated list)
- the API utility transforms these more readable properties into model style (or into Ghost style)
### Model access (`withRelated`)
- e.g. `models.Post.findPage({withRelated: ['tags']})`
- driven by bookshelf
---
Commits explained.
* Reorder the usage of `convertOptions`
- 1. validation
- 2. options convertion
- 3. permissions
- the reason is simple, the permission layer access the model layer
- we have to prepare the options before talking to the model layer
- added `convertOptions` where it was missed (not required, but for consistency reasons)
* Use `withRelated` when accessing the model layer and use `include` when accessing the API layer
* Change `convertOptions` API utiliy
- API Usage
- ghost.api(..., {include: 'tags,authors'})
- `include` should only be used when calling the API (either via request or via manual usage)
- `include` is only for readability and easier format
- Ghost (Model Layer Usage)
- models.Post.findOne(..., {withRelated: ['tags', 'authors']})
- should only use `withRelated`
- model layer cannot read 'tags,authors`
- model layer has no idea what `include` means, speaks a different language
- `withRelated` is bookshelf
- internal usage
* include-count plugin: use `withRelated` instead of `include`
- imagine you outsource this plugin to git and publish it to npm
- `include` is an unknown option in bookshelf
* Updated `permittedOptions` in base model
- `include` is no longer a known option
* Remove all occurances of `include` in the model layer
* Extend `filterOptions` base function
- this function should be called as first action
- we clone the unfiltered options
- check if you are using `include` (this is a protection which could help us in the beginning)
- check for permitted and (later on default `withRelated`) options
- the usage is coming in next commit
* Ensure we call `filterOptions` as first action
- use `ghostBookshelf.Model.filterOptions` as first action
- consistent naming pattern for incoming options: `unfilteredOptions`
- re-added allowed options for `toJSON`
- one unsolved architecture problem:
- if you override a function e.g. `edit`
- then you should call `filterOptions` as first action
- the base implementation of e.g. `edit` will call it again
- future improvement
* Removed `findOne` from Invite model
- no longer needed, the base implementation is the same
2018-02-15 12:53:53 +03:00
|
|
|
return Users.forge().fetch(options).then(function then(users) {
|
2015-06-14 18:58:49 +03:00
|
|
|
var userWithEmail = users.find(function findUser(user) {
|
2014-01-15 02:47:17 +04:00
|
|
|
return user.get('email').toLowerCase() === email.toLowerCase();
|
|
|
|
});
|
2019-01-21 23:53:11 +03:00
|
|
|
|
2014-01-15 02:47:17 +04:00
|
|
|
if (userWithEmail) {
|
2014-08-17 10:17:23 +04:00
|
|
|
return userWithEmail;
|
2014-01-15 02:47:17 +04:00
|
|
|
}
|
|
|
|
});
|
2017-03-13 15:03:26 +03:00
|
|
|
},
|
|
|
|
inactiveStates: inactiveStates
|
2013-06-25 15:43:15 +04:00
|
|
|
});
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2013-09-23 02:20:08 +04:00
|
|
|
Users = ghostBookshelf.Collection.extend({
|
2013-06-25 15:43:15 +04:00
|
|
|
model: User
|
|
|
|
});
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
module.exports = {
|
2014-07-13 15:17:18 +04:00
|
|
|
User: ghostBookshelf.model('User', User),
|
|
|
|
Users: ghostBookshelf.collection('Users', Users)
|
2013-08-06 03:49:06 +04:00
|
|
|
};
|