Ghost/core/server/models/user.js

828 lines
30 KiB
JavaScript
Raw Normal View History

var _ = require('lodash'),
Promise = require('bluebird'),
bcrypt = require('bcryptjs'),
validator = require('validator'),
🎨 refactor the importer (#8473) refs #5422 - we can support null titles after this PR if we want - user model: fix getAuthorRole - user model: support adding roles by name - we support this for roles as well, this makes it easier when importing related user roles (because usually roles already exists in the database and the related id's are wrong e.g. roles_users) - base model: support for null created_at or updated_at values - post or tag slugs are always safe strings - enable an import of a null slug, no need to crash or to cover this on import layer - add new DataImporter logic - uses a class inheritance mechanism to achieve an easier readability and maintenance - schema validation (happens on model layer) was ignored - allow to import unknown user id's (see https://github.com/TryGhost/Ghost/issues/8365) - most of the duplication handling happens on model layer (we can use the power of unique fields and errors from the database) - the import is splitted into three steps: - beforeImport --> prepares the data to import, sorts out relations (roles, tags), detects fields (for LTS) - doImport --> does the actual import - afterImport --> updates the data after successful import e.g. update all user reference fields e.g. published_by (compares the imported data with the current state of the database) - import images: markdown can be null - show error message when json handler can't parse file - do not request gravatar if email is null - return problems/warnings after successful import - optimise warnings in importer - do not return warnings for role duplications, no helpful information - error handler: return context information of error - we show the affected json entries as one line in the UI - show warning for: detected duplicated tag - schema validation: fix valueMustBeBoolean translation - remove context property from json parse error
2017-05-23 19:18:13 +03:00
ObjectId = require('bson-objectid'),
ghostBookshelf = require('./base'),
✨ 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
baseUtils = require('./base/utils'),
errors = require('../errors'),
logging = require('../logging'),
utils = require('../utils'),
gravatar = require('../utils/gravatar'),
validation = require('../data/validation'),
events = require('../events'),
i18n = require('../i18n'),
pipeline = require('../utils/pipeline'),
bcryptGenSalt = Promise.promisify(bcrypt.genSalt),
bcryptHash = Promise.promisify(bcrypt.hash),
bcryptCompare = Promise.promisify(bcrypt.compare),
activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'],
/**
* inactive: owner user before blog setup, suspended users
* locked user: imported users, they get a random passport
*/
inactiveStates = ['inactive', 'locked'],
allStates = activeStates.concat(inactiveStates),
User,
Users;
function validatePasswordLength(password) {
return validator.isLength(password, 8);
}
/**
* generate a random salt and then hash the password with that salt
*/
function generatePasswordHash(password) {
return bcryptGenSalt().then(function (salt) {
return bcryptHash(password, salt);
});
}
User = ghostBookshelf.Model.extend({
tableName: 'users',
defaults: function defaults() {
var baseDefaults = ghostBookshelf.Model.prototype.defaults.call(this);
return _.merge({
password: utils.uid(50)
}, baseDefaults);
},
emitChange: function emitChange(event, options) {
events.emit('user' + '.' + event, this, options);
},
onDestroyed: function onDestroyed(model, response, options) {
if (_.includes(activeStates, model.previous('status'))) {
model.emitChange('deactivated', options);
}
model.emitChange('deleted');
},
onCreated: function onCreated(model) {
model.emitChange('added');
// 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'))) {
model.emitChange('activated');
}
},
onUpdated: function onUpdated(model, response, options) {
model.statusChanging = model.get('status') !== model.updated('status');
model.isActive = _.includes(activeStates, model.get('status'));
if (model.statusChanging) {
model.emitChange(model.isActive ? 'activated' : 'deactivated', options);
} else {
if (model.isActive) {
model.emitChange('activated.edited');
}
}
model.emitChange('edited');
},
isActive: function isActive() {
return activeStates.indexOf(this.get('status')) !== -1;
},
isLocked: function isLocked() {
return this.get('status') === 'locked';
},
isInactive: function isInactive() {
return this.get('status') === 'inactive';
},
/**
* Lookup Gravatar if email changes to update image url
* Generating a slug requires a db call to look for conflicting slugs
*/
onSaving: function onSaving(newPage, attr, options) {
var self = this,
tasks = [];
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
// If the user's email is set & has changed & we are not importing
if (self.hasChanged('email') && self.get('email') && !options.importing) {
tasks.gravatar = (function lookUpGravatar() {
return gravatar.lookup({
email: self.get('email')
}).then(function (response) {
if (response && response.image) {
self.set('profile_image', response.image);
}
});
})();
}
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});
});
})();
}
/**
* 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
*/
if (self.isNew() || self.hasChanged('password')) {
this.set('password', String(this.get('password')));
if (!validatePasswordLength(this.get('password'))) {
return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordDoesNotComplyLength')}));
}
// An import with importOptions supplied can prevent re-hashing a user password
if (options.importPersistUser) {
return;
}
tasks.hashPassword = (function hashPassword() {
return generatePasswordHash(self.get('password'))
.then(function (hash) {
self.set('password', hash);
});
})();
}
return Promise.props(tasks);
},
// For the user model ONLY it is possible to disable validations.
// This is used to bypass validation during the credential check, and must never be done with user-provided data
// Should be removed when #3691 is done
onValidate: function validate() {
var opts = arguments[1],
userData;
if (opts && _.has(opts, 'validate') && opts.validate === false) {
return;
}
// use the base toJSON since this model's overridden toJSON
// removes fields and we want everything to run through the validator.
userData = ghostBookshelf.Model.prototype.toJSON.call(this);
return validation.validateSchema(this.tableName, userData);
},
toJSON: function toJSON(options) {
options = options || {};
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
// remove password hash for security reasons
delete attrs.password;
delete attrs.ghost_auth_access_token;
if (!options || !options.context || (!options.context.user && !options.context.internal)) {
delete attrs.email;
}
return attrs;
},
format: function format(options) {
if (!_.isEmpty(options.website) &&
!validator.isURL(options.website, {
require_protocol: true,
protocols: ['http', 'https']})) {
options.website = 'http://' + options.website;
}
return ghostBookshelf.Model.prototype.format.call(this, options);
},
posts: function posts() {
return this.hasMany('Posts', 'created_by');
},
roles: function roles() {
return this.belongsToMany('Role');
},
permissions: function permissions() {
return this.belongsToMany('Permission');
},
hasRole: function hasRole(roleName) {
var roles = this.related('roles');
return roles.some(function getRole(role) {
return role.get('name') === roleName;
});
},
enforcedFilters: function enforcedFilters() {
if (this.isInternalContext()) {
return null;
}
return this.isPublicContext() ? 'status:[' + allStates.join(',') + ']' : null;
},
defaultFilters: function defaultFilters() {
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
2016-10-12 18:18:57 +03:00
if (this.isInternalContext()) {
return null;
}
return this.isPublicContext() ? null : 'status:[' + allStates.join(',') + ']';
}
}, {
orderDefaultOptions: function orderDefaultOptions() {
return {
last_seen: 'DESC',
name: 'ASC',
created_at: 'DESC'
};
},
/**
* @deprecated in favour of filter
*/
processOptions: function processOptions(options) {
if (!options.status) {
return options;
}
// This is the only place that 'options.where' is set now
options.where = {statements: []};
var value;
// Filter on the status. A status of 'all' translates to no filter since we want all statuses
if (options.status !== 'all') {
// make sure that status is valid
options.status = allStates.indexOf(options.status) > -1 ? options.status : 'active';
}
if (options.status === 'active') {
value = activeStates;
} else if (options.status === 'all') {
value = allStates;
} else {
value = options.status;
}
options.where.statements.push({prop: 'status', op: 'IN', value: value});
delete options.status;
return options;
},
/**
* 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.
*/
permittedOptions: function permittedOptions(methodName) {
var options = ghostBookshelf.Model.permittedOptions(),
// whitelists for the `options` hash argument on methods, by method name.
// these are the only options that can be passed to Bookshelf / Knex.
validOptions = {
findOne: ['withRelated', 'status'],
setup: ['id'],
edit: ['withRelated', 'id', 'importPersistUser'],
add: ['importPersistUser'],
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status'],
findAll: ['filter']
};
if (validOptions[methodName]) {
options = options.concat(validOptions[methodName]);
}
return options;
},
/**
* ### Find One
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
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!
*
* @extends ghostBookshelf.Model.findOne to include roles
* **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
*/
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
2016-10-12 18:18:57 +03:00
findOne: function findOne(dataToClone, options) {
var query,
status,
optInc,
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
2016-10-12 18:18:57 +03:00
data = _.cloneDeep(dataToClone),
lookupRole = data.role;
delete data.role;
data = _.defaults(data || {}, {
status: 'all'
});
status = data.status;
delete data.status;
options = _.cloneDeep(options || {});
optInc = options.include;
options.withRelated = _.union(options.withRelated, options.include);
data = this.filterData(data);
// Support finding by role
if (lookupRole) {
options.withRelated = _.union(options.withRelated, ['roles']);
options.include = _.union(options.include, ['roles']);
query = this.forge(data, {include: options.include});
2016-05-22 11:37:44 +03:00
query.query('join', 'roles_users', 'users.id', '=', 'roles_users.user_id');
query.query('join', 'roles', 'roles_users.role_id', '=', 'roles.id');
query.query('where', 'roles.name', '=', lookupRole);
} else {
// We pass include to forge so that toJSON has access
query = this.forge(data, {include: options.include});
}
if (status === 'active') {
query.query('whereIn', 'status', activeStates);
} else if (status !== 'all') {
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
2016-10-12 18:18:57 +03:00
query.query('where', {status: status});
}
options = this.filterOptions(options, 'findOne');
delete options.include;
options.include = optInc;
return query.fetch(options);
},
/**
* ### Edit
*
* Note: In case of login the last_seen attribute gets updated.
*
* @extends ghostBookshelf.Model.edit to handle returning the full object
* **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
*/
edit: function edit(data, options) {
var self = this,
ops = [];
if (data.roles && data.roles.length > 1) {
return Promise.reject(
new errors.ValidationError({message: i18n.t('errors.models.user.onlyOneRolePerUserSupported')})
);
}
options = options || {};
options.withRelated = _.union(options.withRelated, options.include);
if (data.email) {
ops.push(function checkForDuplicateEmail() {
return self.getByEmail(data.email, options).then(function then(user) {
if (user && user.id !== options.id) {
return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.userUpdateError.emailIsAlreadyInUse')}));
}
});
});
}
ops.push(function update() {
return ghostBookshelf.Model.edit.call(self, data, options).then(function then(user) {
var roleId;
if (!data.roles) {
return user;
}
roleId = data.roles[0].id || data.roles[0];
return user.roles().fetch().then(function then(roles) {
// return if the role is already assigned
if (roles.models[0].id === roleId) {
return;
}
return ghostBookshelf.model('Role').findOne({id: roleId});
}).then(function then(roleToAssign) {
if (roleToAssign && roleToAssign.get('name') === 'Owner') {
return Promise.reject(
new errors.ValidationError({message: i18n.t('errors.models.user.methodDoesNotSupportOwnerRole')})
);
} else {
// assign all other roles
return user.roles().updatePivot({role_id: roleId});
}
}).then(function then() {
options.status = 'all';
return self.findOne({id: user.id}, options);
});
});
});
return pipeline(ops);
},
/**
* ## Add
* Naive user add
* Hashes the password provided before saving to the database.
*
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
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
* @param {object} options
* @extends ghostBookshelf.Model.add to manage all aspects of user signup
* **See:** [ghostBookshelf.Model.add](base.js.html#Add)
*/
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
2016-10-12 18:18:57 +03:00
add: function add(dataToClone, options) {
2013-08-16 03:22:08 +04:00
var self = this,
✨ migrations: seeding is part of init db task (#7545) * 🎨 move heart of fixtures to schema folder and change user model - add fixtures.json to schema folder - add fixture utils to schema folder - keep all the logic! --> FIXTURE.JSON - add owner user with roles --> USER MODEL - add password as default - findAll: allow querying inactive users when internal context (defaultFilters) - findOne: do not remove values from original object! - add: do not remove values from original object! * 🔥 remove migrations key from default_settings.json - this was a temporary invention for an older migration script - sephiroth keep alls needed information in a migration collection * 🔥 add code property to errors - add code property to errors - IMPORTANT: please share your opinion about that - this is a copy paste behaviour of how node is doing that (errno, code etc.) - so code specifies a GhostError * 🎨 change error handling in versioning - no need to throw specific database errors anymore (this was just a temporary solution) - now: we are throwing real DatabaseVersionErrors - specified by a code - background: the versioning unit has not idea about seeding and population of the database - it just throws what it knows --> database version does not exist or settings table does not exist * 🎨 sephiroth optimisations - added getPath function to get the path to init scripts and migration scripts - migrationPath is still hardcoded (see TODO) - tidy up database naming to transacting * ✨ migration init scripts are now complete - 1. add tables - 2. add fixtures - 3. add default settings * 🎨 important: make bootup script smaller! - remove all TODO'S except of one - no seeding logic in bootup script anymore 🕵🏻 * ✨ sephiroth: allow params for init command - param: skip (do not run this script) - param: only (only run this script) - very simple way * 🎨 adapt tests and test env - do not use migrate.populate anymore - use sephiroth instead - jscs/jshint * 🎨 fix User model status checks
2016-10-12 18:18:57 +03:00
data = _.cloneDeep(dataToClone),
userData = this.filterData(data),
roles;
options = this.filterOptions(options, 'add');
options.withRelated = _.union(options.withRelated, options.include);
// check for too many roles
if (data.roles && data.roles.length > 1) {
return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.onlyOneRolePerUserSupported')}));
}
function getAuthorRole() {
🎨 refactor the importer (#8473) refs #5422 - we can support null titles after this PR if we want - user model: fix getAuthorRole - user model: support adding roles by name - we support this for roles as well, this makes it easier when importing related user roles (because usually roles already exists in the database and the related id's are wrong e.g. roles_users) - base model: support for null created_at or updated_at values - post or tag slugs are always safe strings - enable an import of a null slug, no need to crash or to cover this on import layer - add new DataImporter logic - uses a class inheritance mechanism to achieve an easier readability and maintenance - schema validation (happens on model layer) was ignored - allow to import unknown user id's (see https://github.com/TryGhost/Ghost/issues/8365) - most of the duplication handling happens on model layer (we can use the power of unique fields and errors from the database) - the import is splitted into three steps: - beforeImport --> prepares the data to import, sorts out relations (roles, tags), detects fields (for LTS) - doImport --> does the actual import - afterImport --> updates the data after successful import e.g. update all user reference fields e.g. published_by (compares the imported data with the current state of the database) - import images: markdown can be null - show error message when json handler can't parse file - do not request gravatar if email is null - return problems/warnings after successful import - optimise warnings in importer - do not return warnings for role duplications, no helpful information - error handler: return context information of error - we show the affected json entries as one line in the UI - show warning for: detected duplicated tag - schema validation: fix valueMustBeBoolean translation - remove context property from json parse error
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')];
});
}
✨ 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
*/
🎨 refactor the importer (#8473) refs #5422 - we can support null titles after this PR if we want - user model: fix getAuthorRole - user model: support adding roles by name - we support this for roles as well, this makes it easier when importing related user roles (because usually roles already exists in the database and the related id's are wrong e.g. roles_users) - base model: support for null created_at or updated_at values - post or tag slugs are always safe strings - enable an import of a null slug, no need to crash or to cover this on import layer - add new DataImporter logic - uses a class inheritance mechanism to achieve an easier readability and maintenance - schema validation (happens on model layer) was ignored - allow to import unknown user id's (see https://github.com/TryGhost/Ghost/issues/8365) - most of the duplication handling happens on model layer (we can use the power of unique fields and errors from the database) - the import is splitted into three steps: - beforeImport --> prepares the data to import, sorts out relations (roles, tags), detects fields (for LTS) - doImport --> does the actual import - afterImport --> updates the data after successful import e.g. update all user reference fields e.g. published_by (compares the imported data with the current state of the database) - import images: markdown can be null - show error message when json handler can't parse file - do not request gravatar if email is null - return problems/warnings after successful import - optimise warnings in importer - do not return warnings for role duplications, no helpful information - error handler: return context information of error - we show the affected json entries as one line in the UI - show warning for: detected duplicated tag - schema validation: fix valueMustBeBoolean translation - remove context property from json parse error
2017-05-23 19:18:13 +03:00
roles = data.roles;
delete data.roles;
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;
🎨 refactor the importer (#8473) refs #5422 - we can support null titles after this PR if we want - user model: fix getAuthorRole - user model: support adding roles by name - we support this for roles as well, this makes it easier when importing related user roles (because usually roles already exists in the database and the related id's are wrong e.g. roles_users) - base model: support for null created_at or updated_at values - post or tag slugs are always safe strings - enable an import of a null slug, no need to crash or to cover this on import layer - add new DataImporter logic - uses a class inheritance mechanism to achieve an easier readability and maintenance - schema validation (happens on model layer) was ignored - allow to import unknown user id's (see https://github.com/TryGhost/Ghost/issues/8365) - most of the duplication handling happens on model layer (we can use the power of unique fields and errors from the database) - the import is splitted into three steps: - beforeImport --> prepares the data to import, sorts out relations (roles, tags), detects fields (for LTS) - doImport --> does the actual import - afterImport --> updates the data after successful import e.g. update all user reference fields e.g. published_by (compares the imported data with the current state of the database) - import images: markdown can be null - show error message when json handler can't parse file - do not request gravatar if email is null - return problems/warnings after successful import - optimise warnings in importer - do not return warnings for role duplications, no helpful information - error handler: return context information of error - we show the affected json entries as one line in the UI - show warning for: detected duplicated tag - schema validation: fix valueMustBeBoolean translation - remove context property from json parse error
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);
});
});
}
🎨 refactor the importer (#8473) refs #5422 - we can support null titles after this PR if we want - user model: fix getAuthorRole - user model: support adding roles by name - we support this for roles as well, this makes it easier when importing related user roles (because usually roles already exists in the database and the related id's are wrong e.g. roles_users) - base model: support for null created_at or updated_at values - post or tag slugs are always safe strings - enable an import of a null slug, no need to crash or to cover this on import layer - add new DataImporter logic - uses a class inheritance mechanism to achieve an easier readability and maintenance - schema validation (happens on model layer) was ignored - allow to import unknown user id's (see https://github.com/TryGhost/Ghost/issues/8365) - most of the duplication handling happens on model layer (we can use the power of unique fields and errors from the database) - the import is splitted into three steps: - beforeImport --> prepares the data to import, sorts out relations (roles, tags), detects fields (for LTS) - doImport --> does the actual import - afterImport --> updates the data after successful import e.g. update all user reference fields e.g. published_by (compares the imported data with the current state of the database) - import images: markdown can be null - show error message when json handler can't parse file - do not request gravatar if email is null - return problems/warnings after successful import - optimise warnings in importer - do not return warnings for role duplications, no helpful information - error handler: return context information of error - we show the affected json entries as one line in the UI - show warning for: detected duplicated tag - schema validation: fix valueMustBeBoolean translation - remove context property from json parse error
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);
🎨 refactor the importer (#8473) refs #5422 - we can support null titles after this PR if we want - user model: fix getAuthorRole - user model: support adding roles by name - we support this for roles as well, this makes it easier when importing related user roles (because usually roles already exists in the database and the related id's are wrong e.g. roles_users) - base model: support for null created_at or updated_at values - post or tag slugs are always safe strings - enable an import of a null slug, no need to crash or to cover this on import layer - add new DataImporter logic - uses a class inheritance mechanism to achieve an easier readability and maintenance - schema validation (happens on model layer) was ignored - allow to import unknown user id's (see https://github.com/TryGhost/Ghost/issues/8365) - most of the duplication handling happens on model layer (we can use the power of unique fields and errors from the database) - the import is splitted into three steps: - beforeImport --> prepares the data to import, sorts out relations (roles, tags), detects fields (for LTS) - doImport --> does the actual import - afterImport --> updates the data after successful import e.g. update all user reference fields e.g. published_by (compares the imported data with the current state of the database) - import images: markdown can be null - show error message when json handler can't parse file - do not request gravatar if email is null - return problems/warnings after successful import - optimise warnings in importer - do not return warnings for role duplications, no helpful information - error handler: return context information of error - we show the affected json entries as one line in the UI - show warning for: detected duplicated tag - schema validation: fix valueMustBeBoolean translation - remove context property from json parse error
2017-05-23 19:18:13 +03:00
})
.then(function then() {
// find and return the added user
return self.findOne({id: userData.id, status: 'all'}, options);
});
},
/**
* We override the owner!
* Owner already has a slug -> force setting a new one by setting slug to null
* @TODO: kill setup function?
*/
setup: function setup(data, options) {
var self = this,
userData = this.filterData(data);
if (!validatePasswordLength(userData.password)) {
return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.passwordDoesNotComplyLength')}));
}
options = this.filterOptions(options, 'setup');
options.withRelated = _.union(options.withRelated, options.include);
userData.slug = null;
return self.edit.call(self, userData, options);
},
/**
* Right now the setup of the blog depends on the user status.
* 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
*/
isSetup: function isSetup(options) {
return this.getOwnerUser(options)
.then(function (owner) {
return owner.get('status') !== 'inactive';
});
},
getOwnerUser: function getOwnerUser(options) {
options = options || {};
return this.findOne({
role: 'Owner',
status: 'all'
}, options).then(function (owner) {
if (!owner) {
return Promise.reject(new errors.NotFoundError({
message: i18n.t('errors.models.user.ownerNotFound')
}));
}
return owner;
});
},
permissible: function permissible(userModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
var self = this,
userModel = userModelOrId,
origArgs;
// 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
if (_.isNumber(userModelOrId) || _.isString(userModelOrId)) {
// Grab the original args without the first one
origArgs = _.toArray(arguments).slice(1);
// Get the actual user model
return this.findOne({id: userModelOrId, status: 'all'}, {include: ['roles']}).then(function then(foundUserModel) {
if (!foundUserModel) {
throw new errors.NotFoundError({
message: i18n.t('errors.models.user.userNotFound')
});
}
// Build up the original args but substitute with actual model
var newArgs = [foundUserModel].concat(origArgs);
return self.permissible.apply(self, newArgs);
🎨 configurable logging with bunyan (#7431) - 🛠 add bunyan and prettyjson, remove morgan - ✨ add logging module - GhostLogger class that handles setup of bunyan - PrettyStream for stdout - ✨ config for logging - @TODO: testing level fatal? - ✨ log each request via GhostLogger (express middleware) - @TODO: add errors to output - 🔥 remove errors.updateActiveTheme - we can read the value from config - 🔥 remove 15 helper functions in core/server/errors/index.js - all these functions get replaced by modules: 1. logging 2. error middleware handling for html/json 3. error creation (which will be part of PR #7477) - ✨ add express error handler for html/json - one true error handler for express responses - contains still some TODO's, but they are not high priority for first implementation/integration - this middleware only takes responsibility of either rendering html responses or return json error responses - 🎨 use new express error handler in middleware/index - 404 and 500 handling - 🎨 return error instead of error message in permissions/index.js - the rule for error handling should be: if you call a unit, this unit should return a custom Ghost error - 🎨 wrap serve static module - rule: if you call a module/unit, you should always wrap this error - it's always the same rule - so the caller never has to worry about what comes back - it's always a clear error instance - in this case: we return our notfounderror if serve static does not find the resource - this avoid having checks everywhere - 🎨 replace usages of errors/index.js functions and adapt tests - use logging.error, logging.warn - make tests green - remove some usages of logging and throwing api errors -> because when a request is involved, logging happens automatically - 🐛 return errorDetails to Ghost-Admin - errorDetails is used for Theme error handling - 🎨 use 500er error for theme is missing error in theme-handler - 🎨 extend file rotation to 1w
2016-10-04 18:33:43 +03:00
});
}
if (action === 'edit') {
// Owner can only be edited by owner
if (loadedPermissions.user && userModel.hasRole('Owner')) {
hasUserPermission = _.some(loadedPermissions.user.roles, {name: 'Owner'});
}
// Users with the role 'Editor' and 'Author' have complex permissions when the action === 'edit'
// We now have all the info we need to construct the permissions
if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Author'})) {
// If this is the same user that requests the operation allow it.
hasUserPermission = hasUserPermission || context.user === userModel.get('id');
}
if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
// If this is the same user that requests the operation allow it.
hasUserPermission = context.user === userModel.get('id');
// Alternatively, if the user we are trying to edit is an Author, allow it
hasUserPermission = hasUserPermission || userModel.hasRole('Author');
}
}
if (action === 'destroy') {
// Owner cannot be deleted EVER
if (loadedPermissions.user && userModel.hasRole('Owner')) {
return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.notEnoughPermission')}));
}
// Users with the role 'Editor' have complex permissions when the action === 'destroy'
if (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'})) {
// If this is the same user that requests the operation allow it.
hasUserPermission = context.user === userModel.get('id');
// Alternatively, if the user we are trying to edit is an Author, allow it
hasUserPermission = hasUserPermission || userModel.hasRole('Author');
}
}
if (hasUserPermission && hasAppPermission) {
return Promise.resolve();
}
return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.notEnoughPermission')}));
},
// Finds the user by email, and checks the password
// @TODO: shorten this function and rename...
check: function check(object) {
var self = this;
return this.getByEmail(object.email).then(function then(user) {
if (!user) {
return Promise.reject(new errors.NotFoundError({
message: i18n.t('errors.models.user.noUserWithEnteredEmailAddr')
}));
}
if (user.isLocked()) {
return Promise.reject(new errors.NoPermissionError({
message: i18n.t('errors.models.user.accountLocked')
}));
}
if (user.isInactive()) {
return Promise.reject(new errors.NoPermissionError({
message: i18n.t('errors.models.user.accountSuspended')
}));
}
return self.isPasswordCorrect({plainPassword: object.password, hashedPassword: user.get('password')})
.then(function then() {
return Promise.resolve(user.set({status: 'active', last_seen: new Date()}).save({validate: false}))
.catch(function handleError(err) {
// If we get a validation or other error during this save, catch it and log it, but don't
// cause a login error because of it. The user validation is not important here.
logging.error(new errors.GhostError({
err: err,
context: i18n.t('errors.models.user.userUpdateError.context'),
help: i18n.t('errors.models.user.userUpdateError.help')
}));
return user;
});
})
.catch(function onError(err) {
return Promise.reject(err);
});
}, function handleError(error) {
if (error.message === 'NotFound' || error.message === 'EmptyResponse') {
return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.models.user.noUserWithEnteredEmailAddr')}));
}
return Promise.reject(error);
});
},
isPasswordCorrect: function isPasswordCorrect(object) {
var plainPassword = object.plainPassword,
hashedPassword = object.hashedPassword;
if (!plainPassword || !hashedPassword) {
return Promise.reject(new errors.ValidationError({
message: i18n.t('errors.models.user.passwordRequiredForOperation')
}));
}
return bcryptCompare(plainPassword, hashedPassword)
.then(function (matched) {
if (matched) {
return;
}
return Promise.reject(new errors.ValidationError({
context: i18n.t('errors.models.user.incorrectPassword'),
message: i18n.t('errors.models.user.incorrectPassword'),
help: i18n.t('errors.models.user.userUpdateError.help'),
code: 'PASSWORD_INCORRECT'
}));
});
},
/**
* Naive change password method
* @param {Object} object
* @param {Object} options
*/
changePassword: function changePassword(object, options) {
var self = this,
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,
oldPassword = object.oldPassword,
isLoggedInUser = userId === options.context.user,
user;
return self.forge({id: userId}).fetch({require: true})
.then(function then(_user) {
user = _user;
if (isLoggedInUser) {
return self.isPasswordCorrect({
plainPassword: oldPassword,
hashedPassword: user.get('password')
});
}
})
.then(function then() {
return user.save({password: newPassword});
});
},
transferOwnership: function transferOwnership(object, options) {
var ownerRole,
contextUser;
return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Owner'}),
User.findOne({id: options.context.user}, {include: ['roles']}))
.then(function then(results) {
ownerRole = results[0];
contextUser = results[1];
// check if user has the owner role
var currentRoles = contextUser.toJSON(options).roles;
if (!_.some(currentRoles, {id: ownerRole.id})) {
return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.user.onlyOwnerCanTransferOwnerRole')}));
}
return Promise.join(ghostBookshelf.model('Role').findOne({name: 'Administrator'}),
User.findOne({id: object.id}, {include: ['roles']}));
}).then(function then(results) {
var adminRole = results[0],
user = results[1],
currentRoles = user.toJSON(options).roles;
if (!_.some(currentRoles, {id: adminRole.id})) {
return Promise.reject(new errors.ValidationError({message: i18n.t('errors.models.user.onlyAdmCanBeAssignedOwnerRole')}));
}
// convert owner to admin
return Promise.join(contextUser.roles().updatePivot({role_id: adminRole.id}),
user.roles().updatePivot({role_id: ownerRole.id}),
user.id);
}).then(function then(results) {
return Users.forge()
.query('whereIn', 'id', [contextUser.id, results[2]])
.fetch({withRelated: ['roles']});
}).then(function then(users) {
options.include = ['roles'];
return users.toJSON(options);
});
},
// Get the user by email address, enforces case insensitivity rejects if the user is not found
// When multi-user support is added, email addresses must be deduplicated with case insensitivity, so that
// joe@bloggs.com and JOE@BLOGGS.COM cannot be created as two separate users.
getByEmail: function getByEmail(email, options) {
options = options || {};
// We fetch all users and process them in JS as there is no easy way to make this query across all DBs
// Although they all support `lower()`, sqlite can't case transform unicode characters
// This is somewhat mute, as validator.isEmail() also doesn't support unicode, but this is much easier / more
// likely to be fixed in the near future.
options.require = true;
return Users.forge(options).fetch(options).then(function then(users) {
var userWithEmail = users.find(function findUser(user) {
return user.get('email').toLowerCase() === email.toLowerCase();
});
if (userWithEmail) {
return userWithEmail;
}
});
},
inactiveStates: inactiveStates
});
2013-06-01 18:47:41 +04:00
Users = ghostBookshelf.Collection.extend({
model: User
});
2013-06-01 18:47:41 +04:00
module.exports = {
User: ghostBookshelf.model('User', User),
Users: ghostBookshelf.collection('Users', Users)
};