From daa77c5c0074fde77fea0399a75e88678b629cea Mon Sep 17 00:00:00 2001 From: Naz Gargol Date: Tue, 8 Oct 2019 15:44:27 +0200 Subject: [PATCH 01/21] Permission restrictions for `post.visibility` modifications (#11213) no issue - Limited posts visibility field permissions to Editor-Up + Admin Integrations - We don't want contributors or other roles lower than Editor to be able to modify content gating attribute --- core/server/api/canary/pages.js | 2 +- core/server/api/canary/posts.js | 2 +- core/server/api/v2/pages.js | 2 +- core/server/api/v2/posts.js | 2 +- core/server/models/post.js | 16 +++++- core/test/unit/models/post_spec.js | 86 ++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 5 deletions(-) diff --git a/core/server/api/canary/pages.js b/core/server/api/canary/pages.js index 233460f722..bae973dd10 100644 --- a/core/server/api/canary/pages.js +++ b/core/server/api/canary/pages.js @@ -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', diff --git a/core/server/api/canary/posts.js b/core/server/api/canary/posts.js index 6680d201dc..7cc5561910 100644 --- a/core/server/api/canary/posts.js +++ b/core/server/api/canary/posts.js @@ -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', diff --git a/core/server/api/v2/pages.js b/core/server/api/v2/pages.js index 233460f722..bae973dd10 100644 --- a/core/server/api/v2/pages.js +++ b/core/server/api/v2/pages.js @@ -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', diff --git a/core/server/api/v2/posts.js b/core/server/api/v2/posts.js index 6680d201dc..7cc5561910 100644 --- a/core/server/api/v2/posts.js +++ b/core/server/api/v2/posts.js @@ -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', diff --git a/core/server/models/post.js b/core/server/models/post.js index 3f46723aa0..04f25d3afb 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -901,7 +901,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); @@ -916,6 +923,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'); @@ -929,6 +941,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 = []; diff --git a/core/test/unit/models/post_spec.js b/core/test/unit/models/post_spec.js index 46c14a9a15..340f33f6e4 100644 --- a/core/test/unit/models/post_spec.js +++ b/core/test/unit/models/post_spec.js @@ -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(); + }); + }); }); }); }); From 7dc2eb2a1e2d5840b7d96a57ef40da6d14cede7e Mon Sep 17 00:00:00 2001 From: Rishabh Garg Date: Tue, 8 Oct 2019 22:00:46 +0530 Subject: [PATCH 02/21] Added new `requirePaymentForSignup` setting for members (#11214) * Added new `requirePaymentForSignup` setting for members no issue - Adds new `requirePaymentForSignup` setting flag for members, `false` by default. - Wired members API `allowSelfSignup` to `requirePayment` setting --- core/server/data/schema/default-settings.json | 2 +- core/server/services/members/api.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/server/data/schema/default-settings.json b/core/server/data/schema/default-settings.json index a8b4a704cc..6dd3c54f83 100644 --- a/core/server/data/schema/default-settings.json +++ b/core/server/data/schema/default-settings.json @@ -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,\"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\":\"\"}]}}]}" } } } diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 9f9861927b..5d73d28eaa 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -119,6 +119,11 @@ function getStripePaymentConfig() { }; } +function getRequirePaymentSetting() { + const subscriptionSettings = settingsCache.get('members_subscription_settings'); + return !!subscriptionSettings.requirePaymentForSignup; +} + module.exports = createApiInstance; function createApiInstance() { @@ -134,7 +139,8 @@ function createApiInstance() { signinURL.searchParams.set('token', token); signinURL.searchParams.set('action', type); return signinURL.href; - } + }, + allowSelfSignup: !getRequirePaymentSetting() }, mail: { transporter: { From 079a64e46bf2536d1e959e9d38c75d713be32d55 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 11:47:01 +0700 Subject: [PATCH 03/21] Exposed @member.firstname in the theme data no-issue This is very basic split on whitespace for now --- core/frontend/services/themes/middleware.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/frontend/services/themes/middleware.js b/core/frontend/services/themes/middleware.js index 71888a5375..9b8c203c72 100644 --- a/core/frontend/services/themes/middleware.js +++ b/core/frontend/services/themes/middleware.js @@ -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; From dd419be2fb90363cc35a3d585e343ab7e8cc3eb6 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 11:53:58 +0700 Subject: [PATCH 04/21] Added guard for missing stripe tokens no-issue This ensures that even if a stripe config object is present, we still ensure that stripe is configured without keys --- core/server/services/members/api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 5d73d28eaa..3ac8e4a6c6 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -95,6 +95,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); From 1e731dcdd38fa6d37332d894962cb2cac42d843b Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 13:21:23 +0700 Subject: [PATCH 05/21] Removed token param on page load for members no-issue This adds a bit of protection from accidentally sharing the url, and also makes the url look cleaner --- core/server/public/members.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/server/public/members.js b/core/server/public/members.js index cd05056023..719037bb2e 100644 --- a/core/server/public/members.js +++ b/core/server/public/members.js @@ -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); +} From 786eaac57ecdbfcbc4c7969f3e04c1023b537927 Mon Sep 17 00:00:00 2001 From: Naz Gargol Date: Wed, 9 Oct 2019 10:26:54 +0200 Subject: [PATCH 06/21] Added permission restrictions to editing members flag (#11217) no issue - Added test cases to check edit permission on settings endpoints - Added test to demonstrate owner-only being able to toggle members flag - Permission check when editing settings `lab.members` - Passed additional function to permissions to allow custom selection of unsafe attributes due to settings object structure. - Fully implementing this check on controller level would be wrong architecturally and not that straight forward because we lack role data in "frame" - Cleaned up test after moving default_content_visibility to it's own property --- core/server/api/canary/settings.js | 3 + core/server/api/canary/utils/permissions.js | 7 +- core/server/api/v2/settings.js | 3 + core/server/models/settings.js | 30 + .../acceptance/old/admin/settings_spec.js | 4 +- .../api/canary/admin/settings_spec.js | 568 +++++++++++++----- .../regression/api/v2/admin/settings_spec.js | 568 +++++++++++++----- 7 files changed, 852 insertions(+), 331 deletions(-) diff --git a/core/server/api/canary/settings.js b/core/server/api/canary/settings.js index 7927a7461e..0cd5f0c157 100644 --- a/core/server/api/canary/settings.js +++ b/core/server/api/canary/settings.js @@ -83,6 +83,9 @@ module.exports = { cacheInvalidate: true }, permissions: { + unsafeAttrsObject(frame) { + return _.find(frame.data.settings, {key: 'labs'}); + }, before(frame) { const errors = []; diff --git a/core/server/api/canary/utils/permissions.js b/core/server/api/canary/utils/permissions.js index 0feb51a4f9..c704f20e43 100644 --- a/core/server/api/canary/utils/permissions.js +++ b/core/server/api/canary/utils/permissions.js @@ -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) => { diff --git a/core/server/api/v2/settings.js b/core/server/api/v2/settings.js index 7927a7461e..0cd5f0c157 100644 --- a/core/server/api/v2/settings.js +++ b/core/server/api/v2/settings.js @@ -83,6 +83,9 @@ module.exports = { cacheInvalidate: true }, permissions: { + unsafeAttrsObject(frame) { + return _.find(frame.data.settings, {key: 'labs'}); + }, before(frame) { const errors = []; diff --git a/core/server/models/settings.js b/core/server/models/settings.js index 6623ea67ba..5de36857b5 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -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') + })); } }); diff --git a/core/test/acceptance/old/admin/settings_spec.js b/core/test/acceptance/old/admin/settings_spec.js index 493262f0e3..630925da30 100644 --- a/core/test/acceptance/old/admin/settings_spec.js +++ b/core/test/acceptance/old/admin/settings_spec.js @@ -153,7 +153,7 @@ describe('Settings API', function () { }, { key: 'labs', - value: '{"subscribers":false,"members":true,"default_content_visibility":"paid"}' + value: '{"subscribers":false,"members":true}' } ] }; @@ -219,7 +219,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(); diff --git a/core/test/regression/api/canary/admin/settings_spec.js b/core/test/regression/api/canary/admin/settings_spec.js index 698ad1b0ad..27c14a44c3 100644 --- a/core/test/regression/api/canary/admin/settings_spec.js +++ b/core/test/regression/api/canary/admin/settings_spec.js @@ -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(); + }); + }); + }); }); }); diff --git a/core/test/regression/api/v2/admin/settings_spec.js b/core/test/regression/api/v2/admin/settings_spec.js index 698ad1b0ad..27c14a44c3 100644 --- a/core/test/regression/api/v2/admin/settings_spec.js +++ b/core/test/regression/api/v2/admin/settings_spec.js @@ -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(); + }); + }); + }); }); }); From 312e0cc31eca31f3491b8677e8ab109e0f6f351f Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 10:54:02 +0700 Subject: [PATCH 07/21] Installed @tryghost/members-ssr@0.7.0 no-issue This removes the cookie caching functionality from members-ssr --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bd22ff5864..4bcf724624 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@nexes/nql": "0.3.0", "@tryghost/helpers": "1.1.12", "@tryghost/members-api": "0.7.7", - "@tryghost/members-ssr": "0.6.0", + "@tryghost/members-ssr": "0.7.0", "@tryghost/social-urls": "0.1.2", "@tryghost/string": "^0.1.3", "@tryghost/url-utils": "0.6.1", diff --git a/yarn.lock b/yarn.lock index 0dea3688c4..4aea319bef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From a4ff87a774f01776ad7eeef7f9f36cf6299c30a7 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Mon, 7 Oct 2019 11:54:20 +0700 Subject: [PATCH 08/21] Added stripe subscriptions & updated customers table no-issue --- core/server/data/schema/schema.js | 21 ++++++++++++++++++++ core/test/acceptance/old/admin/db_spec.js | 4 +--- core/test/unit/data/schema/integrity_spec.js | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/server/data/schema/schema.js b/core/server/data/schema/schema.js index 6cf11a3600..d18303f4f6 100644 --- a/core/server/data/schema/schema.js +++ b/core/server/data/schema/schema.js @@ -396,11 +396,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} }, + 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}, diff --git a/core/test/acceptance/old/admin/db_spec.js b/core/test/acceptance/old/admin/db_spec.js index 6a5075a32f..23c3a100fc 100644 --- a/core/test/acceptance/old/admin/db_spec.js +++ b/core/test/acceptance/old/admin/db_spec.js @@ -66,7 +66,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); }); }); @@ -100,7 +100,6 @@ describe('DB API', function () { .expect(200) .then((res) => { let jsonResponse = res.body; - let results = jsonResponse.posts; jsonResponse.posts.should.have.length(7); }); }); @@ -115,7 +114,6 @@ describe('DB API', function () { .expect(200) .then((res) => { let jsonResponse = res.body; - let results = jsonResponse.posts; jsonResponse.posts.should.have.length(7); }) .then(() => { diff --git a/core/test/unit/data/schema/integrity_spec.js b/core/test/unit/data/schema/integrity_spec.js index 8e4728f0a8..975b6e7bad 100644 --- a/core/test/unit/data/schema/integrity_spec.js +++ b/core/test/unit/data/schema/integrity_spec.js @@ -19,7 +19,7 @@ var should = require('should'), */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = '2aa35a7d6de956293a08def6cf9d9ecc'; + const currentSchemaHash = '7458175ceb3eba6750700f23461cd4fb'; const currentFixturesHash = 'c7b485fe2f16517295bd35c761129729'; // If this test is failing, then it is likely a change has been made that requires a DB version bump, From 0c32dfaa306032863f38d791968cc561aceaffea Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Mon, 7 Oct 2019 11:54:43 +0700 Subject: [PATCH 09/21] Added migrations for stripe tables no-issue --- ...dd-stripe-customers-subscriptions-table.js | 34 +++++++++++++++++++ ...email-to-members-stripe-customers-table.js | 28 +++++++++++++++ ...-name-to-members-stripe-customers-table.js | 28 +++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js create mode 100644 core/server/data/migrations/versions/3.0/02-add-email-to-members-stripe-customers-table.js create mode 100644 core/server/data/migrations/versions/3.0/03-add-name-to-members-stripe-customers-table.js diff --git a/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js b/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js new file mode 100644 index 0000000000..24a22722b2 --- /dev/null +++ b/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js @@ -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('stripe_customers_subscriptions'); + + if (hasTable) { + common.logging.warn('Adding table: stripe_customers_subscriptions'); + return; + } + + common.logging.info('Adding table: stripe_customers_subscriptions'); + return commands.createTable('stripe_customers_subscriptions', conn); + }, + + async down(options){ + const conn = options.transacting || options.connection; + const hasTable = await conn.schema.hasTable('stripe_customers_subscriptions'); + + if (!hasTable) { + common.logging.warn('Dropping table: stripe_customers_subscriptions'); + return; + } + + common.logging.info('Dropping table: stripe_customers_subscriptions'); + return commands.deleteTable('stripe_customers_subscriptions', conn); + } +}; diff --git a/core/server/data/migrations/versions/3.0/02-add-email-to-members-stripe-customers-table.js b/core/server/data/migrations/versions/3.0/02-add-email-to-members-stripe-customers-table.js new file mode 100644 index 0000000000..f023a86087 --- /dev/null +++ b/core/server/data/migrations/versions/3.0/02-add-email-to-members-stripe-customers-table.js @@ -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 + } +}; diff --git a/core/server/data/migrations/versions/3.0/03-add-name-to-members-stripe-customers-table.js b/core/server/data/migrations/versions/3.0/03-add-name-to-members-stripe-customers-table.js new file mode 100644 index 0000000000..b0fb49b7a0 --- /dev/null +++ b/core/server/data/migrations/versions/3.0/03-add-name-to-members-stripe-customers-table.js @@ -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 + } +}; From 37bb12afb3425320ee3d68124192cfbdd286e275 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Mon, 7 Oct 2019 12:03:24 +0700 Subject: [PATCH 10/21] Added model for stripe_customers_subscriptions no-issue --- core/server/models/index.js | 1 + core/server/models/stripe-customer-subscription.js | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 core/server/models/stripe-customer-subscription.js diff --git a/core/server/models/index.js b/core/server/models/index.js index 6a7e32454b..e40f0ff7c6 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -39,6 +39,7 @@ models = [ 'mobiledoc-revision', 'member', 'member-stripe-customer', + 'stripe-customer-subscription', 'action' ]; diff --git a/core/server/models/stripe-customer-subscription.js b/core/server/models/stripe-customer-subscription.js new file mode 100644 index 0000000000..6ae199b2e2 --- /dev/null +++ b/core/server/models/stripe-customer-subscription.js @@ -0,0 +1,9 @@ +const ghostBookshelf = require('./base'); + +const StripeCustomerSubscription = ghostBookshelf.Model.extend({ + tableName: 'stripe_customers_subscriptions' +}); + +module.exports = { + StripeCustomerSubscription: ghostBookshelf.model('StripeCustomerSubscription', StripeCustomerSubscription) +}; From a6354d1acb6c87e83135f549949c08d6f4d47b29 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 8 Oct 2019 11:16:51 +0700 Subject: [PATCH 11/21] Updated members api to store/retrieve subscriptions no-issue --- core/server/services/members/api.js | 71 ++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 3ac8e4a6c6..1dd7d3b868 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -32,9 +32,55 @@ async function setMemberMetadata(member, module, metadata) { if (module !== 'stripe') { return; } - await models.Member.edit({ - stripe_customers: metadata - }, {id: member.id, withRelated: ['stripe_customers']}); + + if (metadata.customer) { + const model = models.MemberStripeCustomer.forge({ + member_id: member.id, + customer_id: metadata.customer.customer_id + }).where({ + member_id: member.id, + customer_id: metadata.customer.customer_id + }); + + try { + await model.save(metadata.customer, { + method: 'update' + }); + } catch (err) { + if (!(err instanceof models.MemberStripeCustomer.NoRowsUpdatedError)) { + throw err; + } + console.log(err); + await model.save(metadata.customer, { + method: 'insert' + }); + } + } + + if (metadata.subscription) { + const model = await models.StripeCustomerSubscription.forge({ + customer_id: metadata.subscription.customer_id, + subscription_id: metadata.subscription.subscription_id + }).where({ + customer_id: metadata.subscription.customer_id, + subscription_id: metadata.subscription.subscription_id + }); + + try { + await model.save(metadata.subscription, { + method: 'update' + }); + } catch (err) { + if (!(err instanceof models.StripeCustomerSubscription.NoRowsUpdatedError)) { + throw err; + } + console.log(err); + await model.save(metadata.subscription, { + method: 'insert' + }); + } + } + return; } @@ -42,9 +88,22 @@ async function getMemberMetadata(member, module) { 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.where({ + member_id: member.id + }).fetchAll()).toJSON(); + + const subscriptions = await customers.reduce(async (subscriptionsPromise, customer) => { + const customerSubscriptions = await models.StripeCustomerSubscription.where({ + customer_id: customer.customer_id + }).fetchAll(); + return (await subscriptionsPromise).concat(customerSubscriptions.toJSON()); + }, []); + + return { + customers: customers, + subscriptions: subscriptions + }; } function updateMember({name}, options) { From 998642eb24bf8a05972a606bb1eff1d1858b797a Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 8 Oct 2019 13:05:33 +0700 Subject: [PATCH 12/21] Allowed `filter` option for `findAll` method no-issue This will allow us to constrain findAll queries, rather than using knex `where` & `fetchAll` methods --- core/server/models/base/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 83657ded8d..86a8de635d 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -675,7 +675,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ case 'findOne': return baseOptions.concat(extraOptions, ['columns', 'require']); case 'findAll': - return baseOptions.concat(extraOptions, ['columns']); + return baseOptions.concat(extraOptions, ['filter', 'columns']); case 'findPage': return baseOptions.concat(extraOptions, ['filter', 'order', 'page', 'limit', 'columns']); default: From 3366bd1254700551a308421addee5166956ffdc6 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 8 Oct 2019 13:10:20 +0700 Subject: [PATCH 13/21] Added upsert method to stripe models no-issue This is kind of copied from the session model, but simplified This will allow much easier integration with members-api --- core/server/models/member-stripe-customer.js | 11 +++++++++++ core/server/models/stripe-customer-subscription.js | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/core/server/models/member-stripe-customer.js b/core/server/models/member-stripe-customer.js index d5580fcb11..6a4807d0a6 100644 --- a/core/server/models/member-stripe-customer.js +++ b/core/server/models/member-stripe-customer.js @@ -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 = { diff --git a/core/server/models/stripe-customer-subscription.js b/core/server/models/stripe-customer-subscription.js index 6ae199b2e2..5f9c51881d 100644 --- a/core/server/models/stripe-customer-subscription.js +++ b/core/server/models/stripe-customer-subscription.js @@ -2,6 +2,17 @@ const ghostBookshelf = require('./base'); const StripeCustomerSubscription = ghostBookshelf.Model.extend({ tableName: '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 = { From ee0449245acca4b00265b509749cbd0daf465e53 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 8 Oct 2019 13:11:30 +0700 Subject: [PATCH 14/21] Updated setMemberMetadata to use upsert method no-issue Much cleaner now :) --- core/server/services/members/api.js | 40 ++--------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index 1dd7d3b868..ae8443d23b 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -34,51 +34,15 @@ async function setMemberMetadata(member, module, metadata) { } if (metadata.customer) { - const model = models.MemberStripeCustomer.forge({ - member_id: member.id, - customer_id: metadata.customer.customer_id - }).where({ - member_id: member.id, + await models.MemberStripeCustomer.upsert(metadata.customer, { customer_id: metadata.customer.customer_id }); - - try { - await model.save(metadata.customer, { - method: 'update' - }); - } catch (err) { - if (!(err instanceof models.MemberStripeCustomer.NoRowsUpdatedError)) { - throw err; - } - console.log(err); - await model.save(metadata.customer, { - method: 'insert' - }); - } } if (metadata.subscription) { - const model = await models.StripeCustomerSubscription.forge({ - customer_id: metadata.subscription.customer_id, - subscription_id: metadata.subscription.subscription_id - }).where({ - customer_id: metadata.subscription.customer_id, + await models.StripeCustomerSubscription.upsert(metadata.subscription, { subscription_id: metadata.subscription.subscription_id }); - - try { - await model.save(metadata.subscription, { - method: 'update' - }); - } catch (err) { - if (!(err instanceof models.StripeCustomerSubscription.NoRowsUpdatedError)) { - throw err; - } - console.log(err); - await model.save(metadata.subscription, { - method: 'insert' - }); - } } return; From 4c07d860866c455131134a8d9560afeb5fee6cb3 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 8 Oct 2019 13:12:01 +0700 Subject: [PATCH 15/21] Updated getMemberMetadata to use findAll method no-issue This means we go via our version of the bookshelf model --- core/server/services/members/api.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index ae8443d23b..ee03880ba7 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -53,14 +53,14 @@ async function getMemberMetadata(member, module) { return; } - const customers = (await models.MemberStripeCustomer.where({ - member_id: member.id - }).fetchAll()).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.where({ - customer_id: customer.customer_id - }).fetchAll(); + const customerSubscriptions = await models.StripeCustomerSubscription.findAll({ + filter: `customer_id:${customer.customer_id}` + }); return (await subscriptionsPromise).concat(customerSubscriptions.toJSON()); }, []); From 757fe72da1dc9031db7e625d9ea9e5fea26ecb62 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 10:55:02 +0700 Subject: [PATCH 16/21] Installed @tryghost/members-api@0.8.0 no-issue This adds support for storing and retrieving stripe data from local db --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4bcf724624..f62b7e020e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "dependencies": { "@nexes/nql": "0.3.0", "@tryghost/helpers": "1.1.12", - "@tryghost/members-api": "0.7.7", + "@tryghost/members-api": "0.8.0", "@tryghost/members-ssr": "0.7.0", "@tryghost/social-urls": "0.1.2", "@tryghost/string": "^0.1.3", diff --git a/yarn.lock b/yarn.lock index 4aea319bef..4c16196b28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 29b3dad3025433e27a07a9d1b0520330b996aa74 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Tue, 8 Oct 2019 16:32:26 +0700 Subject: [PATCH 17/21] Updated get/set metadata fn signatures no-issue This is to reflect an upstream change in members-api --- core/server/services/members/api.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/server/services/members/api.js b/core/server/services/members/api.js index ee03880ba7..901d3f452c 100644 --- a/core/server/services/members/api.js +++ b/core/server/services/members/api.js @@ -28,7 +28,7 @@ function getMember(data, options = {}) { }); } -async function setMemberMetadata(member, module, metadata) { +async function setMetadata(module, metadata) { if (module !== 'stripe') { return; } @@ -48,7 +48,7 @@ async function setMemberMetadata(member, module, metadata) { return; } -async function getMemberMetadata(member, module) { +async function getMetadata(module, member) { if (module !== 'stripe') { return; } @@ -204,8 +204,8 @@ function createApiInstance() { paymentConfig: { stripe: getStripePaymentConfig() }, - setMemberMetadata, - getMemberMetadata, + setMetadata, + getMetadata, createMember, updateMember, getMember, From cbb6337ae4857dcaa669385a87c006a46dac74a2 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 12:37:57 +0700 Subject: [PATCH 18/21] Prefixed stripe_customers_subscriptions with members no-issue --- ...1-add-stripe-customers-subscriptions-table.js | 16 ++++++++-------- core/server/data/schema/schema.js | 2 +- .../models/stripe-customer-subscription.js | 2 +- core/test/unit/data/schema/integrity_spec.js | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js b/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js index 24a22722b2..8c9062416e 100644 --- a/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js +++ b/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js @@ -8,27 +8,27 @@ module.exports = { async up(options){ const conn = options.transacting || options.connection; - const hasTable = await conn.schema.hasTable('stripe_customers_subscriptions'); + const hasTable = await conn.schema.hasTable('members_stripe_customers_subscriptions'); if (hasTable) { - common.logging.warn('Adding table: stripe_customers_subscriptions'); + common.logging.warn('Adding table: members_stripe_customers_subscriptions'); return; } - common.logging.info('Adding table: stripe_customers_subscriptions'); - return commands.createTable('stripe_customers_subscriptions', conn); + 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('stripe_customers_subscriptions'); + const hasTable = await conn.schema.hasTable('members_stripe_customers_subscriptions'); if (!hasTable) { - common.logging.warn('Dropping table: stripe_customers_subscriptions'); + common.logging.warn('Dropping table: members_stripe_customers_subscriptions'); return; } - common.logging.info('Dropping table: stripe_customers_subscriptions'); - return commands.deleteTable('stripe_customers_subscriptions', conn); + common.logging.info('Dropping table: members_stripe_customers_subscriptions'); + return commands.deleteTable('members_stripe_customers_subscriptions', conn); } }; diff --git a/core/server/data/schema/schema.js b/core/server/data/schema/schema.js index d18303f4f6..b519e63c7d 100644 --- a/core/server/data/schema/schema.js +++ b/core/server/data/schema/schema.js @@ -403,7 +403,7 @@ module.exports = { updated_at: {type: 'dateTime', nullable: true}, updated_by: {type: 'string', maxlength: 24, nullable: true} }, - stripe_customers_subscriptions: { + 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}, diff --git a/core/server/models/stripe-customer-subscription.js b/core/server/models/stripe-customer-subscription.js index 5f9c51881d..8b4d40d03b 100644 --- a/core/server/models/stripe-customer-subscription.js +++ b/core/server/models/stripe-customer-subscription.js @@ -1,7 +1,7 @@ const ghostBookshelf = require('./base'); const StripeCustomerSubscription = ghostBookshelf.Model.extend({ - tableName: 'stripe_customers_subscriptions' + tableName: 'members_stripe_customers_subscriptions' }, { async upsert(data, unfilteredOptions) { const subscriptionId = unfilteredOptions.subscription_id; diff --git a/core/test/unit/data/schema/integrity_spec.js b/core/test/unit/data/schema/integrity_spec.js index 975b6e7bad..891814c867 100644 --- a/core/test/unit/data/schema/integrity_spec.js +++ b/core/test/unit/data/schema/integrity_spec.js @@ -19,7 +19,7 @@ var should = require('should'), */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = '7458175ceb3eba6750700f23461cd4fb'; + const currentSchemaHash = 'a5341373370dc25009806963c1bc236f'; const currentFixturesHash = 'c7b485fe2f16517295bd35c761129729'; // If this test is failing, then it is likely a change has been made that requires a DB version bump, From b77026870b96765fde26d14db80cf752a5954de9 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 9 Oct 2019 16:18:58 +0700 Subject: [PATCH 19/21] Moved migrations from 3.0 to 2.34 no-issue --- .../{3.0 => 2.34}/01-add-stripe-customers-subscriptions-table.js | 0 .../02-add-email-to-members-stripe-customers-table.js | 0 .../03-add-name-to-members-stripe-customers-table.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename core/server/data/migrations/versions/{3.0 => 2.34}/01-add-stripe-customers-subscriptions-table.js (100%) rename core/server/data/migrations/versions/{3.0 => 2.34}/02-add-email-to-members-stripe-customers-table.js (100%) rename core/server/data/migrations/versions/{3.0 => 2.34}/03-add-name-to-members-stripe-customers-table.js (100%) diff --git a/core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js b/core/server/data/migrations/versions/2.34/01-add-stripe-customers-subscriptions-table.js similarity index 100% rename from core/server/data/migrations/versions/3.0/01-add-stripe-customers-subscriptions-table.js rename to core/server/data/migrations/versions/2.34/01-add-stripe-customers-subscriptions-table.js diff --git a/core/server/data/migrations/versions/3.0/02-add-email-to-members-stripe-customers-table.js b/core/server/data/migrations/versions/2.34/02-add-email-to-members-stripe-customers-table.js similarity index 100% rename from core/server/data/migrations/versions/3.0/02-add-email-to-members-stripe-customers-table.js rename to core/server/data/migrations/versions/2.34/02-add-email-to-members-stripe-customers-table.js diff --git a/core/server/data/migrations/versions/3.0/03-add-name-to-members-stripe-customers-table.js b/core/server/data/migrations/versions/2.34/03-add-name-to-members-stripe-customers-table.js similarity index 100% rename from core/server/data/migrations/versions/3.0/03-add-name-to-members-stripe-customers-table.js rename to core/server/data/migrations/versions/2.34/03-add-name-to-members-stripe-customers-table.js From e7d7d9fdcc62669db186ba19326b966d77b90ec6 Mon Sep 17 00:00:00 2001 From: Rish Date: Wed, 9 Oct 2019 15:28:09 +0530 Subject: [PATCH 20/21] Added new `fromAddress` setting for member subscriptions no issue - Adds new `fromAddress` setting for member subscriptions to allow custom from mail address --- core/server/data/schema/default-settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/server/data/schema/default-settings.json b/core/server/data/schema/default-settings.json index 6dd3c54f83..88b3c8c966 100644 --- a/core/server/data/schema/default-settings.json +++ b/core/server/data/schema/default-settings.json @@ -198,7 +198,7 @@ "defaultValue": "public" }, "members_subscription_settings": { - "defaultValue": "{\"isPaid\":false,\"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\":\"\"}]}}]}" + "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\":\"\"}]}}]}" } } } From b4548b011994f41ce33ea275562d885bc6df4fec Mon Sep 17 00:00:00 2001 From: Naz Gargol Date: Wed, 9 Oct 2019 12:13:12 +0200 Subject: [PATCH 21/21] Update dependency gscan to v2.10.0 (#11221) no issue - This removes unwanted checks for `{{statusCode}}` which are compatible with v3 and were added by mistake in v2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f62b7e020e..70ca7e685f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 4c16196b28..e8d26131ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3740,10 +3740,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"