c6a95c6478
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
214 lines
7.9 KiB
JavaScript
214 lines
7.9 KiB
JavaScript
var _ = require('lodash'),
|
|
models = require('../../models'),
|
|
common = require('../../lib/common'),
|
|
security = require('../../lib/security'),
|
|
strategies;
|
|
|
|
strategies = {
|
|
|
|
/**
|
|
* ClientPasswordStrategy
|
|
*
|
|
* This strategy is used to authenticate registered OAuth clients. It is
|
|
* employed to protect the `token` endpoint, which consumers use to obtain
|
|
* access tokens. The OAuth 2.0 specification suggests that clients use the
|
|
* HTTP Basic scheme to authenticate (not implemented yet).
|
|
* Use of the client password strategy is implemented to support ember-simple-auth.
|
|
*/
|
|
clientPasswordStrategy: function clientPasswordStrategy(clientId, clientSecret, done) {
|
|
return models.Client.findOne({slug: clientId}, {withRelated: ['trustedDomains']})
|
|
.then(function then(model) {
|
|
if (model) {
|
|
var client = model.toJSON({withRelated: ['trustedDomains']});
|
|
if (client.status === 'enabled' && client.secret === clientSecret) {
|
|
return done(null, client);
|
|
}
|
|
}
|
|
return done(null, false);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* BearerStrategy
|
|
*
|
|
* This strategy is used to authenticate users based on an access token (aka a
|
|
* bearer token). The user must have previously authorized a client
|
|
* application, which is issued an access token to make requests on behalf of
|
|
* the authorizing user.
|
|
*/
|
|
bearerStrategy: function bearerStrategy(accessToken, done) {
|
|
return models.Accesstoken.findOne({token: accessToken})
|
|
.then(function then(model) {
|
|
if (model) {
|
|
var token = model.toJSON();
|
|
if (token.expires > Date.now()) {
|
|
return models.User.findOne({id: token.user_id})
|
|
.then(function then(model) {
|
|
if (!model) {
|
|
return done(null, false);
|
|
}
|
|
|
|
if (!model.isActive()) {
|
|
throw new common.errors.NoPermissionError({
|
|
message: common.i18n.t('errors.models.user.accountSuspended')
|
|
});
|
|
}
|
|
|
|
var user = model.toJSON(),
|
|
info = {scope: '*'};
|
|
|
|
return done(null, {id: user.id}, info);
|
|
})
|
|
.catch(function (err) {
|
|
return done(err);
|
|
});
|
|
} else {
|
|
return done(null, false);
|
|
}
|
|
} else {
|
|
return done(null, false);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Ghost Strategy
|
|
* ghostAuthRefreshToken: will be null for now, because we don't need it right now
|
|
*
|
|
* CASES:
|
|
* - via invite token
|
|
* - via normal sign in
|
|
* - via setup
|
|
*/
|
|
ghostStrategy: function ghostStrategy(req, ghostAuthAccessToken, ghostAuthRefreshToken, profile, done) {
|
|
var inviteToken = req.body.inviteToken,
|
|
options = {context: {internal: true}},
|
|
handleInviteToken, handleSetup, handleSignIn;
|
|
|
|
// CASE: socket hangs up for example
|
|
if (!ghostAuthAccessToken || !profile) {
|
|
return done(new common.errors.NoPermissionError({
|
|
help: 'Please try again.'
|
|
}));
|
|
}
|
|
|
|
handleInviteToken = function handleInviteToken() {
|
|
var user, invite;
|
|
inviteToken = security.url.decodeBase64(inviteToken);
|
|
|
|
return models.Invite.findOne({token: inviteToken}, options)
|
|
.then(function addInviteUser(_invite) {
|
|
invite = _invite;
|
|
|
|
if (!invite) {
|
|
throw new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.api.invites.inviteNotFound')
|
|
});
|
|
}
|
|
|
|
if (invite.get('expires') < Date.now()) {
|
|
throw new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.api.invites.inviteExpired')
|
|
});
|
|
}
|
|
|
|
return models.User.add({
|
|
email: profile.email,
|
|
name: profile.name,
|
|
password: security.identifier.uid(50),
|
|
roles: [invite.toJSON().role_id],
|
|
ghost_auth_id: profile.id,
|
|
ghost_auth_access_token: ghostAuthAccessToken
|
|
}, options);
|
|
})
|
|
.then(function destroyInvite(_user) {
|
|
user = _user;
|
|
return invite.destroy(options);
|
|
})
|
|
.then(function () {
|
|
return user;
|
|
});
|
|
};
|
|
|
|
handleSetup = function handleSetup() {
|
|
return models.User.findOne({slug: 'ghost-owner', status: 'inactive'}, options)
|
|
.then(function fetchedOwner(owner) {
|
|
if (!owner) {
|
|
throw new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.models.user.userNotFound')
|
|
});
|
|
}
|
|
|
|
// CASE: slug null forces regenerating the slug (ghost-owner is default and needs to be overridden)
|
|
return models.User.edit({
|
|
email: profile.email,
|
|
name: profile.name,
|
|
slug: null,
|
|
status: 'active',
|
|
ghost_auth_id: profile.id,
|
|
ghost_auth_access_token: ghostAuthAccessToken
|
|
}, _.merge({id: owner.id}, options));
|
|
});
|
|
};
|
|
|
|
handleSignIn = function handleSignIn() {
|
|
var user;
|
|
|
|
return models.User.findOne({ghost_auth_id: profile.id}, options)
|
|
.then(function (_user) {
|
|
user = _user;
|
|
|
|
if (!user) {
|
|
throw new common.errors.NotFoundError();
|
|
}
|
|
|
|
if (!user.isActive()) {
|
|
throw new common.errors.NoPermissionError({
|
|
message: common.i18n.t('errors.models.user.accountSuspended')
|
|
});
|
|
}
|
|
|
|
return models.User.edit({
|
|
email: profile.email,
|
|
name: profile.name,
|
|
ghost_auth_id: profile.id,
|
|
ghost_auth_access_token: ghostAuthAccessToken
|
|
}, _.merge({id: user.id}, options));
|
|
})
|
|
.then(function () {
|
|
return user;
|
|
});
|
|
};
|
|
|
|
if (inviteToken) {
|
|
return handleInviteToken()
|
|
.then(function (user) {
|
|
done(null, user, profile);
|
|
})
|
|
.catch(function (err) {
|
|
done(err);
|
|
});
|
|
}
|
|
|
|
handleSignIn()
|
|
.then(function (user) {
|
|
done(null, user, profile);
|
|
})
|
|
.catch(function (err) {
|
|
if (!(err instanceof common.errors.NotFoundError)) {
|
|
return done(err);
|
|
}
|
|
|
|
handleSetup()
|
|
.then(function (user) {
|
|
done(null, user, profile);
|
|
})
|
|
.catch(function (err) {
|
|
done(err);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = strategies;
|