Merge branch 'master' into v3

This commit is contained in:
Kevin Ansfield 2019-10-09 15:04:09 +01:00
commit 587bd8accb
29 changed files with 1176 additions and 374 deletions

View File

@ -81,6 +81,7 @@ function updateLocalTemplateOptions(req, res, next) {
const member = req.member ? {
email: req.member.email,
name: req.member.name,
firstname: req.member.name && req.member.name.split(' ')[0],
subscriptions: req.member.stripe.subscriptions,
paid: req.member.stripe.subscriptions.length !== 0
} : null;

View File

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
const UNSAFE_ATTRS = ['status', 'authors'];
const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'pages',

View File

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
const unsafeAttrs = ['status', 'authors'];
const unsafeAttrs = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'posts',

View File

@ -83,6 +83,9 @@ module.exports = {
cacheInvalidate: true
},
permissions: {
unsafeAttrsObject(frame) {
return _.find(frame.data.settings, {key: 'labs'});
},
before(frame) {
const errors = [];

View File

@ -26,7 +26,12 @@ const nonePublicAuth = (apiConfig, frame) => {
permissionIdentifier = apiConfig.identifier(frame);
}
const unsafeAttrObject = apiConfig.unsafeAttrs && _.has(frame, `data.[${apiConfig.docName}][0]`) ? _.pick(frame.data[apiConfig.docName][0], apiConfig.unsafeAttrs) : {};
let unsafeAttrObject = apiConfig.unsafeAttrs && _.has(frame, `data.[${apiConfig.docName}][0]`) ? _.pick(frame.data[apiConfig.docName][0], apiConfig.unsafeAttrs) : {};
if (apiConfig.unsafeAttrsObject) {
unsafeAttrObject = apiConfig.unsafeAttrsObject(frame);
}
const permsPromise = permissions.canThis(frame.options.context)[apiConfig.method][singular](permissionIdentifier, unsafeAttrObject);
return permsPromise.then((result) => {

View File

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles'];
const UNSAFE_ATTRS = ['status', 'authors'];
const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'pages',

View File

@ -2,7 +2,7 @@ const models = require('../../models');
const common = require('../../lib/common');
const urlUtils = require('../../lib/url-utils');
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
const unsafeAttrs = ['status', 'authors'];
const unsafeAttrs = ['status', 'authors', 'visibility'];
module.exports = {
docName: 'posts',

View File

@ -83,6 +83,9 @@ module.exports = {
cacheInvalidate: true
},
permissions: {
unsafeAttrsObject(frame) {
return _.find(frame.data.settings, {key: 'labs'});
},
before(frame) {
const errors = [];

View File

@ -0,0 +1,34 @@
const common = require('../../../../lib/common');
const commands = require('../../../schema/commands');
module.exports = {
config: {
transaction: true
},
async up(options){
const conn = options.transacting || options.connection;
const hasTable = await conn.schema.hasTable('members_stripe_customers_subscriptions');
if (hasTable) {
common.logging.warn('Adding table: members_stripe_customers_subscriptions');
return;
}
common.logging.info('Adding table: members_stripe_customers_subscriptions');
return commands.createTable('members_stripe_customers_subscriptions', conn);
},
async down(options){
const conn = options.transacting || options.connection;
const hasTable = await conn.schema.hasTable('members_stripe_customers_subscriptions');
if (!hasTable) {
common.logging.warn('Dropping table: members_stripe_customers_subscriptions');
return;
}
common.logging.info('Dropping table: members_stripe_customers_subscriptions');
return commands.deleteTable('members_stripe_customers_subscriptions', conn);
}
};

View File

@ -0,0 +1,28 @@
const commands = require('../../../schema').commands;
module.exports = {
up: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'email',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
}),
down: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'email',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}),
config: {
transaction: true
}
};

View File

@ -0,0 +1,28 @@
const commands = require('../../../schema').commands;
module.exports = {
up: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'name',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
}),
down: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'name',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}),
config: {
transaction: true
}
};

View File

@ -198,7 +198,7 @@
"defaultValue": "public"
},
"members_subscription_settings": {
"defaultValue": "{\"isPaid\":false,\"paymentProcessors\":[{\"adapter\":\"stripe\",\"config\":{\"secret_token\":\"\",\"public_token\":\"\",\"product\":{\"name\":\"Ghost Subscription\"},\"plans\":[{\"name\":\"Monthly\",\"currency\":\"usd\",\"interval\":\"month\",\"amount\":\"\"},{\"name\":\"Yearly\",\"currency\":\"usd\",\"interval\":\"year\",\"amount\":\"\"}]}}]}"
"defaultValue": "{\"isPaid\":false,\"fromAddress\":\"noreply\",\"requirePaymentForSignup\":false,\"paymentProcessors\":[{\"adapter\":\"stripe\",\"config\":{\"secret_token\":\"\",\"public_token\":\"\",\"product\":{\"name\":\"Ghost Subscription\"},\"plans\":[{\"name\":\"Monthly\",\"currency\":\"usd\",\"interval\":\"month\",\"amount\":\"\"},{\"name\":\"Yearly\",\"currency\":\"usd\",\"interval\":\"year\",\"amount\":\"\"}]}}]}"
}
}
}

View File

@ -333,11 +333,32 @@ module.exports = {
member_id: {type: 'string', maxlength: 24, nullable: false, unique: false},
// customer_id is unique: false because mysql with innodb utf8mb4 cannot have unqiue columns larger than 191 chars
customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
name: {type: 'string', maxlength: 191, nullable: true},
email: {type: 'string', maxlength: 191, nullable: true},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'string', maxlength: 24, nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'string', maxlength: 24, nullable: true}
},
members_stripe_customers_subscriptions: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
subscription_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
plan_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
status: {type: 'string', maxlength: 50, nullable: false},
current_period_end: {type: 'dateTime', nullable: false},
start_date: {type: 'dateTime', nullable: false},
default_payment_card_last4: {type: 'string', maxlength: 4, nullable: true},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'string', maxlength: 24, nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'string', maxlength: 24, nullable: true},
/* Below fields eventually should be normalised e.g. stripe_plans table, link to here on plan_id */
plan_nickname: {type: 'string', maxlength: 50, nullable: false},
plan_interval: {type: 'string', maxlength: 50, nullable: false},
plan_amount: {type: 'integer', nullable: false},
plan_currency: {type: 'string', maxLength: 3, nullable: false}
},
actions: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
resource_id: {type: 'string', maxlength: 24, nullable: true},

View File

@ -684,7 +684,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
case 'findOne':
return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']);
case 'findAll':
return baseOptions.concat(extraOptions, ['columns', 'mongoTransformer']);
return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']);
case 'findPage':
return baseOptions.concat(extraOptions, ['filter', 'order', 'page', 'limit', 'columns', 'mongoTransformer']);
default:

View File

@ -35,7 +35,8 @@ models = [
'member',
'action',
'posts-meta',
'member-stripe-customer'
'member-stripe-customer',
'stripe-customer-subscription'
];
function init() {

View File

@ -2,6 +2,17 @@ const ghostBookshelf = require('./base');
const MemberStripeCustomer = ghostBookshelf.Model.extend({
tableName: 'members_stripe_customers'
}, {
async upsert(data, unfilteredOptions) {
const customerId = data.customer_id;
const model = await this.findOne({customer_id: customerId}, unfilteredOptions);
if (model) {
return this.edit(data, Object.assign({}, unfilteredOptions, {
id: model.id
}));
}
return this.add(data, unfilteredOptions);
}
});
module.exports = {

View File

@ -905,7 +905,14 @@ Post = ghostBookshelf.Model.extend({
// NOTE: the `authors` extension is the parent of the post model. It also has a permissible function.
permissible: function permissible(postModel, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
let isContributor, isEdit, isAdd, isDestroy;
let isContributor;
let isOwner;
let isAdmin;
let isEditor;
let isIntegration;
let isEdit;
let isAdd;
let isDestroy;
function isChanging(attr) {
return unsafeAttrs[attr] && unsafeAttrs[attr] !== postModel.get(attr);
@ -920,6 +927,11 @@ Post = ghostBookshelf.Model.extend({
}
isContributor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Contributor'});
isOwner = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
isAdmin = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Admin'});
isEditor = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Editor'});
isIntegration = loadedPermissions.apiKey && _.some(loadedPermissions.apiKey.roles, {name: 'Admin Integration'});
isEdit = (action === 'edit');
isAdd = (action === 'add');
isDestroy = (action === 'destroy');
@ -933,6 +945,8 @@ Post = ghostBookshelf.Model.extend({
} else if (isContributor && isDestroy) {
// If destroying, only allow contributor to destroy their own draft posts
hasUserPermission = isDraft();
} else if (!(isOwner || isAdmin || isEditor || isIntegration)) {
hasUserPermission = !isChanging('visibility');
}
const excludedAttrs = [];

View File

@ -6,6 +6,7 @@ const Promise = require('bluebird'),
ghostBookshelf = require('./base'),
common = require('../lib/common'),
validation = require('../data/validation'),
settingsCache = require('../services/settings/cache'),
internalContext = {context: {internal: true}};
let Settings, defaultSettings;
@ -235,6 +236,35 @@ Settings = ghostBookshelf.Model.extend({
return allSettings;
});
},
permissible: function permissible(modelId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasAppPermission, hasApiKeyPermission) {
let isEdit = (action === 'edit');
let isOwner;
function isChangingMembers() {
if (unsafeAttrs && unsafeAttrs.key === 'labs') {
let editedValue = JSON.parse(unsafeAttrs.value);
if (editedValue.members !== undefined) {
return editedValue.members !== settingsCache.get('labs').members;
}
}
}
isOwner = loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'});
if (isEdit && isChangingMembers()) {
// Only allow owner to toggle members flag
hasUserPermission = isOwner;
}
if (hasUserPermission && hasApiKeyPermission && hasAppPermission) {
return Promise.resolve();
}
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.models.post.notEnoughPermission')
}));
}
});

View File

@ -0,0 +1,20 @@
const ghostBookshelf = require('./base');
const StripeCustomerSubscription = ghostBookshelf.Model.extend({
tableName: 'members_stripe_customers_subscriptions'
}, {
async upsert(data, unfilteredOptions) {
const subscriptionId = unfilteredOptions.subscription_id;
const model = await this.findOne({subscription_id: subscriptionId}, unfilteredOptions);
if (model) {
return this.edit(data, Object.assign({}, unfilteredOptions, {
id: model.id
}));
}
return this.add(data, unfilteredOptions);
}
});
module.exports = {
StripeCustomerSubscription: ghostBookshelf.model('StripeCustomerSubscription', StripeCustomerSubscription)
};

View File

@ -138,3 +138,9 @@ Array.prototype.forEach.call(document.querySelectorAll('[data-members-signout]')
}
el.addEventListener('click', clickHandler);
});
var url = new URL(window.location);
if (url.searchParams.get('token')) {
url.searchParams.delete('token');
window.history.replaceState({}, document.title, url.href);
}

View File

@ -28,23 +28,46 @@ function getMember(data, options = {}) {
});
}
async function setMemberMetadata(member, module, metadata) {
async function setMetadata(module, metadata) {
if (module !== 'stripe') {
return;
}
await models.Member.edit({
stripe_customers: metadata
}, {id: member.id, withRelated: ['stripe_customers']});
if (metadata.customer) {
await models.MemberStripeCustomer.upsert(metadata.customer, {
customer_id: metadata.customer.customer_id
});
}
if (metadata.subscription) {
await models.StripeCustomerSubscription.upsert(metadata.subscription, {
subscription_id: metadata.subscription.subscription_id
});
}
return;
}
async function getMemberMetadata(member, module) {
async function getMetadata(module, member) {
if (module !== 'stripe') {
return;
}
const model = await models.Member.where({id: member.id}).fetch({withRelated: ['stripe_customers']});
const metadata = await model.related('stripe_customers');
return metadata.toJSON();
const customers = (await models.MemberStripeCustomer.findAll({
filter: `member_id:${member.id}`
})).toJSON();
const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => {
const customerSubscriptions = await models.StripeCustomerSubscription.findAll({
filter: `customer_id:${customer.customer_id}`
});
return (await subscriptionsPromise).concat(customerSubscriptions.toJSON());
}, []);
return {
customers: customers,
subscriptions: subscriptions
};
}
function updateMember({name}, options) {
@ -95,6 +118,10 @@ function getStripePaymentConfig() {
return null;
}
if (!stripePaymentProcessor.config.public_token || !stripePaymentProcessor.config.secret_token) {
return null;
}
const webhookHandlerUrl = new URL('/members/webhooks/stripe', siteUrl);
const checkoutSuccessUrl = new URL(siteUrl);
@ -119,6 +146,11 @@ function getStripePaymentConfig() {
};
}
function getRequirePaymentSetting() {
const subscriptionSettings = settingsCache.get('members_subscription_settings');
return !!subscriptionSettings.requirePaymentForSignup;
}
module.exports = createApiInstance;
function createApiInstance() {
@ -134,7 +166,8 @@ function createApiInstance() {
signinURL.searchParams.set('token', token);
signinURL.searchParams.set('action', type);
return signinURL.href;
}
},
allowSelfSignup: !getRequirePaymentSetting()
},
mail: {
transporter: {
@ -171,8 +204,8 @@ function createApiInstance() {
paymentConfig: {
stripe: getStripePaymentConfig()
},
setMemberMetadata,
getMemberMetadata,
setMetadata,
getMetadata,
createMember,
updateMember,
getMember,

View File

@ -55,7 +55,7 @@ describe('DB API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.db);
jsonResponse.db.should.have.length(1);
Object.keys(jsonResponse.db[0].data).length.should.eql(26);
Object.keys(jsonResponse.db[0].data).length.should.eql(27);
});
});
@ -89,7 +89,6 @@ describe('DB API', function () {
.expect(200)
.then((res) => {
let jsonResponse = res.body;
let results = jsonResponse.posts;
jsonResponse.posts.should.have.length(7);
});
});
@ -104,7 +103,6 @@ describe('DB API', function () {
.expect(200)
.then((res) => {
let jsonResponse = res.body;
let results = jsonResponse.posts;
jsonResponse.posts.should.have.length(7);
})
.then(() => {

View File

@ -155,7 +155,7 @@ describe('Settings API', function () {
},
{
key: 'labs',
value: '{"subscribers":false,"members":true,"default_content_visibility":"paid"}'
value: '{"subscribers":false,"members":true}'
}
]
};
@ -221,7 +221,7 @@ describe('Settings API', function () {
should.equal(putBody.settings[12].value, 'twitter description');
putBody.settings[13].key.should.eql('labs');
should.equal(putBody.settings[13].value, '{"subscribers":false,"members":true,"default_content_visibility":"paid"}');
should.equal(putBody.settings[13].value, '{"subscribers":false,"members":true}');
localUtils.API.checkResponse(putBody, 'settings');
done();

View File

@ -4,190 +4,430 @@ const config = require('../../../../../server/config');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
let request;
describe('Settings API', function () {
let ghostServer;
let request;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request);
});
});
describe('As Owner', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request);
});
});
after(function () {
return ghostServer.stop();
});
after(function () {
return ghostServer.stop();
});
it('Can\'t read core setting', function () {
return request
.get(localUtils.API.getApiQuery('settings/db_hash/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
it('Can\'t read core setting', function () {
return request
.get(localUtils.API.getApiQuery('settings/db_hash/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
it('Can\'t read permalinks', function (done) {
request.get(localUtils.API.getApiQuery('settings/permalinks/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
it('Can\'t read permalinks', function (done) {
request.get(localUtils.API.getApiQuery('settings/permalinks/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
done();
});
});
it('can\'t read non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/testsetting/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
it('can\'t read non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/testsetting/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
};
it('can toggle member setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
changedValue = [],
settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":false}'
}
]
};
done();
});
});
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
it('can\'t edit non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'testvalue', value: newValue}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
it('Will transform "1"', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body,
settingToChange = {
settings: [
{
key: 'is_private',
value: '1'
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
const putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
putBody.settings[0].key.should.eql('labs');
putBody.settings[0].value.should.eql(JSON.stringify({subscribers: false, members: false}));
const putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
done();
});
});
});
putBody.settings[0].key.should.eql('is_private');
putBody.settings[0].value.should.eql(true);
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
};
localUtils.API.checkResponse(putBody, 'settings');
done();
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
it('can\'t edit non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'testvalue', value: newValue}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
it('Will transform "1"', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body,
settingToChange = {
settings: [
{
key: 'is_private',
value: '1'
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
putBody.settings[0].key.should.eql('is_private');
putBody.settings[0].value.should.eql(true);
localUtils.API.checkResponse(putBody, 'settings');
done();
});
});
});
});
describe('As Admin', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
// create admin
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'admin+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[0].name
});
});
})
.then(function (admin) {
request.user = admin;
// by default we login with the owner
return localUtils.doAuth(request);
});
});
it('cannot toggle member setting', function (done) {
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":true}'
}
]
};
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
});
describe('As Editor', function () {
let editor;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
// create editor
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[1].name
});
})
.then(function (_user1) {
editor = _user1;
request.user = editor;
// by default we login with the owner
return localUtils.doAuth(request);
});
});
it('should not be able to edit settings', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'visibility', value: 'public'}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
});
describe('As Author', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
// create author
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+2@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[2].name
});
})
.then(function (author) {
request.user = author;
// by default we login with the owner
return localUtils.doAuth(request);
});
});
it('should not be able to edit settings', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'visibility', value: 'public'}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
});
});

View File

@ -4,190 +4,430 @@ const config = require('../../../../../server/config');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
let request;
describe('Settings API', function () {
let ghostServer;
let request;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request);
});
});
describe('As Owner', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request);
});
});
after(function () {
return ghostServer.stop();
});
after(function () {
return ghostServer.stop();
});
it('Can\'t read core setting', function () {
return request
.get(localUtils.API.getApiQuery('settings/db_hash/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
it('Can\'t read core setting', function () {
return request
.get(localUtils.API.getApiQuery('settings/db_hash/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
it('Can\'t read permalinks', function (done) {
request.get(localUtils.API.getApiQuery('settings/permalinks/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
it('Can\'t read permalinks', function (done) {
request.get(localUtils.API.getApiQuery('settings/permalinks/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
done();
});
});
it('can\'t read non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/testsetting/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
it('can\'t read non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/testsetting/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
};
it('can toggle member setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
changedValue = [],
settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":false}'
}
]
};
done();
});
});
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
it('can\'t edit non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'testvalue', value: newValue}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
it('Will transform "1"', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body,
settingToChange = {
settings: [
{
key: 'is_private',
value: '1'
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
const putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
putBody.settings[0].key.should.eql('labs');
putBody.settings[0].value.should.eql(JSON.stringify({subscribers: false, members: false}));
const putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
done();
});
});
});
putBody.settings[0].key.should.eql('is_private');
putBody.settings[0].value.should.eql(true);
it('can\'t edit permalinks', function (done) {
const settingToChange = {
settings: [{key: 'permalinks', value: '/:primary_author/:slug/'}]
};
localUtils.API.checkResponse(putBody, 'settings');
done();
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
it('can\'t edit non existent setting', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'testvalue', value: newValue}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
it('Will transform "1"', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body,
settingToChange = {
settings: [
{
key: 'is_private',
value: '1'
}
]
};
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const putBody = res.body;
res.headers['x-cache-invalidate'].should.eql('/*');
should.exist(putBody);
putBody.settings[0].key.should.eql('is_private');
putBody.settings[0].value.should.eql(true);
localUtils.API.checkResponse(putBody, 'settings');
done();
});
});
});
});
describe('As Admin', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
// create admin
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'admin+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[0].name
});
});
})
.then(function (admin) {
request.user = admin;
// by default we login with the owner
return localUtils.doAuth(request);
});
});
it('cannot toggle member setting', function (done) {
const settingToChange = {
settings: [
{
key: 'labs',
value: '{"subscribers":false,"members":true}'
}
]
};
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(settingToChange)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
done();
});
});
});
describe('As Editor', function () {
let editor;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
// create editor
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+1@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[1].name
});
})
.then(function (_user1) {
editor = _user1;
request.user = editor;
// by default we login with the owner
return localUtils.doAuth(request);
});
});
it('should not be able to edit settings', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'visibility', value: 'public'}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
});
describe('As Author', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
// create author
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({email: 'test+2@ghost.org'}),
role: testUtils.DataGenerator.Content.roles[2].name
});
})
.then(function (author) {
request.user = author;
// by default we login with the owner
return localUtils.doAuth(request);
});
});
it('should not be able to edit settings', function (done) {
request.get(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body,
newValue = 'new value';
should.exist(jsonResponse);
should.exist(jsonResponse.settings);
jsonResponse.settings = [{key: 'visibility', value: 'public'}];
request.put(localUtils.API.getApiQuery('settings/'))
.set('Origin', config.get('url'))
.send(jsonResponse)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403)
.end(function (err, res) {
if (err) {
return done(err);
}
jsonResponse = res.body;
should.not.exist(res.headers['x-cache-invalidate']);
should.exist(jsonResponse.errors);
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
'message',
'context',
'type',
'details',
'property',
'help',
'code',
'id'
]);
done();
});
});
});
});
});

View File

@ -19,7 +19,7 @@ var should = require('should'),
*/
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = 'e08c68dce141db80b1756cb911e76b89';
const currentSchemaHash = 'bf8ffd57c6d35998dc0b17ef9d34f4cc';
const currentFixturesHash = '9e3a7f71cab98f3fb8504d1f234b503d';
// If this test is failing, then it is likely a change has been made that requires a DB version bump,

View File

@ -397,6 +397,36 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () {
});
});
it('rejects if changing visibility', function (done) {
var mockPostObj = {
get: sinon.stub(),
related: sinon.stub()
},
context = {user: 1},
unsafeAttrs = {visibility: 'public'};
mockPostObj.get.withArgs('visibility').returns('paid');
mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]});
models.Post.permissible(
mockPostObj,
'edit',
context,
unsafeAttrs,
testUtils.permissions.contributor,
false,
false,
true
).then(() => {
done(new Error('Permissible function should have rejected.'));
}).catch((error) => {
error.should.be.an.instanceof(common.errors.NoPermissionError);
should(mockPostObj.get.called).be.false();
should(mockPostObj.related.calledOnce).be.true();
done();
});
});
it('rejects if changing author id', function (done) {
var mockPostObj = {
get: sinon.stub(),
@ -869,6 +899,36 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () {
});
});
it('rejects if changing visibility', function (done) {
var mockPostObj = {
get: sinon.stub(),
related: sinon.stub()
},
context = {user: 1},
unsafeAttrs = {visibility: 'public'};
mockPostObj.get.withArgs('visibility').returns('paid');
mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]});
models.Post.permissible(
mockPostObj,
'edit',
context,
unsafeAttrs,
testUtils.permissions.author,
false,
false,
true
).then(() => {
done(new Error('Permissible function should have rejected.'));
}).catch((error) => {
error.should.be.an.instanceof(common.errors.NoPermissionError);
should(mockPostObj.get.called).be.false();
should(mockPostObj.related.calledOnce).be.true();
done();
});
});
it('rejects if editing another\'s post (using `authors`)', function (done) {
var mockPostObj = {
get: sinon.stub(),
@ -1174,6 +1234,32 @@ describe('Unit: models/post: uses database (@TODO: fix me)', function () {
should(mockPostObj.get.called).be.false();
});
});
it('resolves if changing visibility', function () {
var mockPostObj = {
get: sinon.stub(),
related: sinon.stub()
},
context = {user: 1},
unsafeAttrs = {visibility: 'public'};
mockPostObj.get.withArgs('visibility').returns('paid');
mockPostObj.related.withArgs('authors').returns({models: [{id: 1}]});
models.Post.permissible(
mockPostObj,
'edit',
context,
unsafeAttrs,
testUtils.permissions.editor,
false,
true,
true
).then(() => {
should(mockPostObj.get.called).be.false();
should(mockPostObj.related.calledOnce).be.true();
});
});
});
});
});

View File

@ -41,8 +41,8 @@
"dependencies": {
"@nexes/nql": "0.3.0",
"@tryghost/helpers": "1.1.12",
"@tryghost/members-api": "0.7.7",
"@tryghost/members-ssr": "0.6.0",
"@tryghost/members-api": "0.8.0",
"@tryghost/members-ssr": "0.7.0",
"@tryghost/social-urls": "0.1.2",
"@tryghost/string": "^0.1.3",
"@tryghost/url-utils": "0.6.1",
@ -79,7 +79,7 @@
"ghost-storage-base": "0.0.3",
"glob": "7.1.4",
"got": "9.6.0",
"gscan": "2.9.0",
"gscan": "2.10.0",
"html-to-text": "5.1.1",
"image-size": "0.8.3",
"intl": "1.2.5",

View File

@ -227,22 +227,22 @@
dependencies:
"@tryghost/kg-clean-basic-html" "^0.1.3"
"@tryghost/magic-link@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.2.0.tgz#77b6fac7d83fdff0543a1506be63601f1ec9f742"
integrity sha512-vYj48P7RKLMfdkRCdYQ6bGv5J168ce621ZuBhEbGKf6kIiiRfNQDno+FOjiOBBKn3AS+FkIulc3z48qz/0U+Jg==
"@tryghost/magic-link@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.2.1.tgz#c729bf5d2fe7fa1330eccbba51ba3579834784fc"
integrity sha512-bqlZndOXwU3b9FXvMtHIep1EradDnsfQ+4vvINQ+QsCOWKH1EDbPhjYS9f2G0xNx8BVyG4e1eMxZ5lBhJ6lBCA==
dependencies:
bluebird "^3.5.5"
ghost-ignition "^3.1.0"
jsonwebtoken "^8.5.1"
lodash "^4.17.15"
"@tryghost/members-api@0.7.7":
version "0.7.7"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.7.7.tgz#fbb08241a231dc2d651b6dacf3c4cd06e4d3799d"
integrity sha512-a3V61Ti//PCWN3+PmfmL4MUi7ZHuWhROrr2vLfzitw3E/3CRYKXXLgk4qfe9wAVIbzdmNS8caXWXAoeVg+Vzgw==
"@tryghost/members-api@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.8.0.tgz#e1f0b67371b6b61f6cf4f64e5b62c92ba3777c2a"
integrity sha512-THPd9HUyqo1WdroWFH8X+KcNfyy586dVSOYRQWIjF+URoYHmlrf2AlxBrdHMq5R8mQVCKIO0t3pmZwRnafucVA==
dependencies:
"@tryghost/magic-link" "^0.2.0"
"@tryghost/magic-link" "^0.2.1"
bluebird "^3.5.4"
body-parser "^1.19.0"
cookies "^0.7.3"
@ -253,10 +253,10 @@
node-jose "^1.1.3"
stripe "^7.4.0"
"@tryghost/members-ssr@0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-ssr/-/members-ssr-0.6.0.tgz#15a1475407a0c66b479710dffb84624038c2ad8c"
integrity sha512-ZvZ3FuUI6F/Z3MuRMf1nNiixaSNJuYF1h5sXqUd0dIlQbCX/fOaw+57M+ApWcGGJieRvq4qnort6cZxgjQqQ6A==
"@tryghost/members-ssr@0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@tryghost/members-ssr/-/members-ssr-0.7.0.tgz#d4e7a6554375b65efc13651361311f0c960f9cd9"
integrity sha512-DE4xXjIvJBL/JG9wj/6tVajDgbGJ0Qvq4LjNW9gjO2EmeG6W7F9RKPyy4H4hDnSv+g5LG1oz6c/aIkivaQjNOw==
dependencies:
bluebird "^3.5.3"
concat-stream "^2.0.0"
@ -3745,10 +3745,10 @@ grunt@1.0.4:
path-is-absolute "~1.0.0"
rimraf "~2.6.2"
gscan@2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/gscan/-/gscan-2.9.0.tgz#de169bd971043872ac830a65cd632149ecddbb8c"
integrity sha512-igE0rPtbc0u3IQ3pruFXTSarc+JIHEXbRv/iyqeFlujgR0iKJ0tKPKRQGceYqzy5x+sT7MTwtxpa+LLOhN2SNw==
gscan@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/gscan/-/gscan-2.10.0.tgz#62c5a4e685304335e716baf9ec26a3974a158e07"
integrity sha512-Z0R0hEk00L/dfN0FvdGVA1XnE32/kYMFL9fkkpvlRYw9Rwwftp5sEtUAOrxuUA1ysersb9wLlWgunaf4BIzcwA==
dependencies:
"@tryghost/extract-zip" "1.6.6"
"@tryghost/pretty-cli" "1.2.1"