mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 10:53:34 +03:00
Added frontend key to ghost_head for portal (#14782)
refs: https://github.com/TryGhost/Team/issues/1599
refs: f3d5d9cf6b
- this commit adds the concept of a frontend data service, intended for passing data to the frontend from the server in a clean way. This is the start of a new & improved pattern, to hopefully reduce coupling
- the newly added internal frontend key is then exposed through this pattern so that the frontend can make use of it
- the first use case is so that portal can use it to talk to the content API instead of having weird endpoints for portal
- this key will also be used by other internal scripts in future, it's public and therefore safe to expose, but it's meant for internal use only and therefore is not exposed in a generic way e.g. as a helper
This commit is contained in:
parent
2e8c66d93c
commit
409dc3b534
13
core/boot.js
13
core/boot.js
@ -168,15 +168,22 @@ async function initServicesForFrontend({bootLogger}) {
|
||||
await offers.init();
|
||||
debug('End: Offers');
|
||||
|
||||
const frontendDataService = require('./server/services/frontend-data-service');
|
||||
let dataService = await frontendDataService.init();
|
||||
|
||||
debug('End: initServicesForFrontend');
|
||||
return {dataService};
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontend is intended to be just Ghost's frontend
|
||||
*/
|
||||
async function initFrontend() {
|
||||
async function initFrontend(dataService) {
|
||||
debug('Begin: initFrontend');
|
||||
|
||||
const proxyService = require('./frontend/services/proxy');
|
||||
proxyService.init({dataService});
|
||||
|
||||
const helperService = require('./frontend/services/helpers');
|
||||
await helperService.init();
|
||||
|
||||
@ -414,10 +421,10 @@ async function bootGhost({backend = true, frontend = true, server = true} = {})
|
||||
// Step 4 - Load Ghost with all its services
|
||||
debug('Begin: Load Ghost Services & Apps');
|
||||
await initCore({ghostServer, config, bootLogger, frontend});
|
||||
await initServicesForFrontend({bootLogger});
|
||||
const {dataService} = await initServicesForFrontend({bootLogger});
|
||||
|
||||
if (frontend) {
|
||||
await initFrontend();
|
||||
await initFrontend(dataService);
|
||||
}
|
||||
const ghostApp = await initExpressApps({frontend, backend, config});
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
// Usage: `{{ghost_head}}`
|
||||
//
|
||||
// Outputs scripts and other assets at the top of a Ghost theme
|
||||
const {metaData, settingsCache, config, blogIcon, urlUtils, labs} = require('../services/proxy');
|
||||
const {metaData, settingsCache, config, blogIcon, urlUtils, labs, getFrontendKey} = require('../services/proxy');
|
||||
const {escapeExpression, SafeString} = require('../services/handlebars');
|
||||
|
||||
// BAD REQUIRE
|
||||
@ -43,7 +43,7 @@ function finaliseStructuredData(meta) {
|
||||
return head;
|
||||
}
|
||||
|
||||
function getMembersHelper(data) {
|
||||
function getMembersHelper(data, frontendKey) {
|
||||
if (settingsCache.get('members_signup_access') === 'none') {
|
||||
return '';
|
||||
}
|
||||
@ -52,8 +52,7 @@ function getMembersHelper(data) {
|
||||
const stripeDirectPublishableKey = settingsCache.get('stripe_publishable_key');
|
||||
const stripeConnectAccountId = settingsCache.get('stripe_connect_account_id');
|
||||
const colorString = _.has(data, 'site._preview') && data.site.accent_color ? ` data-accent-color="${data.site.accent_color}"` : '';
|
||||
const portalUrl = config.get('portal:url');
|
||||
let membersHelper = `<script defer src="${portalUrl}" data-ghost="${urlUtils.getSiteUrl()}"${colorString} crossorigin="anonymous"></script>`;
|
||||
let membersHelper = `<script defer src="${config.get('portal:url')}" data-ghost="${urlUtils.getSiteUrl()}"${colorString} data-key="${frontendKey}" data-api="${urlUtils.urlFor('api', {type: 'content'}, true)}" crossorigin="anonymous"></script>`;
|
||||
membersHelper += (`<style id="gh-members-styles">${templateStyles}</style>`);
|
||||
if ((!!stripeDirectSecretKey && !!stripeDirectPublishableKey) || !!stripeConnectAccountId) {
|
||||
membersHelper += '<script async src="https://js.stripe.com/v3/"></script>';
|
||||
@ -129,6 +128,8 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
||||
* - it should not break anything
|
||||
*/
|
||||
const meta = await getMetaData(dataRoot, dataRoot);
|
||||
const frontendKey = await getFrontendKey();
|
||||
|
||||
debug('end fetch');
|
||||
|
||||
if (context) {
|
||||
@ -194,7 +195,7 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
|
||||
|
||||
// no code injection for amp context!!!
|
||||
if (!_.includes(context, 'amp')) {
|
||||
head.push(getMembersHelper(options.data));
|
||||
head.push(getMembersHelper(options.data, frontendKey));
|
||||
|
||||
// @TODO do this in a more "frameworky" way
|
||||
if (cardAssetService.hasFile('js')) {
|
||||
|
@ -5,7 +5,13 @@ const config = require('../../shared/config');
|
||||
// Require from the handlebars framework
|
||||
const {SafeString} = require('./handlebars');
|
||||
|
||||
let _dataService = {};
|
||||
|
||||
module.exports = {
|
||||
getFrontendKey: () => {
|
||||
return _dataService.getFrontendKey();
|
||||
},
|
||||
|
||||
/**
|
||||
* Section two: data manipulation
|
||||
* Stuff that modifies API data (SDK layer)
|
||||
@ -47,3 +53,7 @@ module.exports = {
|
||||
urlService: require('../../server/services/url'),
|
||||
urlUtils: require('../../shared/url-utils')
|
||||
};
|
||||
|
||||
module.exports.init = ({dataService}) => {
|
||||
_dataService = dataService;
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
const _ = require('lodash');
|
||||
const limitService = require('../services/limits');
|
||||
const ghostBookshelf = require('./base');
|
||||
const {NoPermissionError} = require('@tryghost/errors');
|
||||
@ -64,6 +65,14 @@ const Integration = ghostBookshelf.Model.extend({
|
||||
return options;
|
||||
},
|
||||
|
||||
defaultRelations: function defaultRelations(methodName, options) {
|
||||
if (['edit', 'add', 'destroy'].indexOf(methodName) !== -1) {
|
||||
options.withRelated = _.union(['api_keys'], options.withRelated || []);
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
async permissible(integrationModel, action, context, attrs, loadedPerms, hasUserPermission, hasApiKeyPermission) {
|
||||
const isAdd = (action === 'add');
|
||||
|
||||
@ -76,6 +85,14 @@ const Integration = ghostBookshelf.Model.extend({
|
||||
if (!hasUserPermission || !hasApiKeyPermission) {
|
||||
throw new NoPermissionError();
|
||||
}
|
||||
},
|
||||
|
||||
async getInternalFrontendKey(options) {
|
||||
options = options || {};
|
||||
|
||||
options.withRelated = ['api_keys'];
|
||||
|
||||
return this.findOne({slug: 'ghost-internal-frontend'}, options);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
const errors = require('@tryghost/errors');
|
||||
const logging = require('@tryghost/logging');
|
||||
|
||||
class FrontendDataService {
|
||||
constructor({IntegrationModel}) {
|
||||
this.IntegrationModel = IntegrationModel;
|
||||
this.frontendKey = null;
|
||||
}
|
||||
|
||||
async getFrontendKey() {
|
||||
if (this.frontendKey) {
|
||||
return this.frontendKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const key = await this.IntegrationModel.getInternalFrontendKey();
|
||||
this.frontendKey = key.toJSON().api_keys[0].secret;
|
||||
} catch (error) {
|
||||
this.frontendKey = null;
|
||||
logging.error(new errors.InternalServerError({message: 'Unable to find the internal frontend key', err: error}));
|
||||
}
|
||||
|
||||
return this.frontendKey;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FrontendDataService;
|
6
core/server/services/frontend-data-service/index.js
Normal file
6
core/server/services/frontend-data-service/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
const models = require('../../models');
|
||||
const FrontendDataService = require('./frontend-data-service');
|
||||
|
||||
module.exports.init = () => {
|
||||
return new FrontendDataService({IntegrationModel: models.Integration});
|
||||
};
|
@ -13,7 +13,8 @@ const routing = require('../../../../core/frontend/services/routing');
|
||||
const urlService = require('../../../../core/server/services/url');
|
||||
|
||||
const ghost_head = require('../../../../core/frontend/helpers/ghost_head');
|
||||
const {settingsCache} = require('../../../../core/frontend/services/proxy');
|
||||
const proxy = require('../../../../core/frontend/services/proxy');
|
||||
const {settingsCache} = proxy;
|
||||
|
||||
describe('{{ghost_head}} helper', function () {
|
||||
let posts = [];
|
||||
@ -21,6 +22,8 @@ describe('{{ghost_head}} helper', function () {
|
||||
let authors = [];
|
||||
let users = [];
|
||||
|
||||
let keyStub;
|
||||
|
||||
const makeFixtures = () => {
|
||||
const {createPost, createUser, createTag} = testUtils.DataGenerator.forKnex;
|
||||
|
||||
@ -278,6 +281,12 @@ describe('{{ghost_head}} helper', function () {
|
||||
before(function () {
|
||||
// @TODO: remove when visibility is refactored out of models
|
||||
models.init();
|
||||
|
||||
keyStub = sinon.stub().resolves('xyz');
|
||||
const dataService = {
|
||||
getFrontendKey: keyStub
|
||||
};
|
||||
proxy.init({dataService});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
@ -1657,6 +1666,7 @@ describe('{{ghost_head}} helper', function () {
|
||||
})).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('<script defer src="https://unpkg.com/@tryghost/portal');
|
||||
rendered.string.should.containEql('data-ghost="http://127.0.0.1:2369/" data-key="xyz" data-api="http://127.0.0.1:2369/ghost/api/content/"');
|
||||
rendered.string.should.containEql('<style id="gh-members-styles">');
|
||||
done();
|
||||
}).catch(done);
|
||||
@ -1674,6 +1684,7 @@ describe('{{ghost_head}} helper', function () {
|
||||
})).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('<script defer src="https://unpkg.com/@tryghost/portal');
|
||||
rendered.string.should.containEql('data-ghost="http://127.0.0.1:2369/" data-key="xyz" data-api="http://127.0.0.1:2369/ghost/api/content/"');
|
||||
rendered.string.should.containEql('<style id="gh-members-styles">');
|
||||
done();
|
||||
}).catch(done);
|
||||
@ -1694,6 +1705,7 @@ describe('{{ghost_head}} helper', function () {
|
||||
})).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('<script defer src="https://unpkg.com/@tryghost/portal');
|
||||
rendered.string.should.containEql('data-ghost="http://127.0.0.1:2369/" data-key="xyz" data-api="http://127.0.0.1:2369/ghost/api/content/"');
|
||||
rendered.string.should.containEql('<style id="gh-members-styles">');
|
||||
rendered.string.should.containEql('<script async src="https://js.stripe.com');
|
||||
done();
|
||||
@ -1715,6 +1727,7 @@ describe('{{ghost_head}} helper', function () {
|
||||
})).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('<script defer src="https://unpkg.com/@tryghost/portal');
|
||||
rendered.string.should.containEql('data-ghost="http://127.0.0.1:2369/" data-key="xyz" data-api="http://127.0.0.1:2369/ghost/api/content/"');
|
||||
rendered.string.should.containEql('<style id="gh-members-styles">');
|
||||
rendered.string.should.containEql('<script async src="https://js.stripe.com');
|
||||
done();
|
||||
@ -1734,6 +1747,7 @@ describe('{{ghost_head}} helper', function () {
|
||||
})).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.not.containEql('<script defer src="https://unpkg.com/@tryghost/portal');
|
||||
rendered.string.should.not.containEql('data-ghost="http://127.0.0.1:2369/" data-key="xyz" data-api="http://127.0.0.1:2369/ghost/api/content/"');
|
||||
rendered.string.should.not.containEql('<style id="gh-members-styles">');
|
||||
rendered.string.should.not.containEql('<script async src="https://js.stripe.com');
|
||||
done();
|
||||
@ -1755,6 +1769,7 @@ describe('{{ghost_head}} helper', function () {
|
||||
})).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('<script defer src="https://unpkg.com/@tryghost/portal');
|
||||
rendered.string.should.containEql('data-ghost="http://127.0.0.1:2369/" data-key="xyz" data-api="http://127.0.0.1:2369/ghost/api/content/"');
|
||||
rendered.string.should.containEql('<style id="gh-members-styles">');
|
||||
rendered.string.should.not.containEql('<script async src="https://js.stripe.com');
|
||||
done();
|
||||
|
@ -68,4 +68,34 @@ describe('Unit: models/integration', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInternalFrontendKey', function () {
|
||||
const mockDb = require('mock-knex');
|
||||
let tracker;
|
||||
|
||||
before(function () {
|
||||
mockDb.mock(knex);
|
||||
tracker = mockDb.getTracker();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
mockDb.unmock(knex);
|
||||
});
|
||||
|
||||
it('generates correct query', function () {
|
||||
const queries = [];
|
||||
tracker.install();
|
||||
|
||||
tracker.on('query', (query) => {
|
||||
queries.push(query);
|
||||
query.response([]);
|
||||
});
|
||||
|
||||
return models.Integration.getInternalFrontendKey().then(() => {
|
||||
queries.length.should.eql(1);
|
||||
queries[0].sql.should.eql('select `integrations`.* from `integrations` where `integrations`.`slug` = ? limit ?');
|
||||
queries[0].bindings.should.eql(['ghost-internal-frontend', 1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,63 @@
|
||||
const assert = require('assert');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const models = require('../../../../../core/server/models');
|
||||
|
||||
const FrontendDataService = require('../../../../../core/server/services/frontend-data-service/frontend-data-service');
|
||||
|
||||
describe('Frontend Data Service', function () {
|
||||
let service, modelStub, fakeModel;
|
||||
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
fakeModel = {
|
||||
toJSON: () => {
|
||||
return {api_keys: [{secret: 'xyz'}]};
|
||||
}
|
||||
};
|
||||
|
||||
modelStub = sinon.stub(models.Integration, 'getInternalFrontendKey');
|
||||
|
||||
service = new FrontendDataService({
|
||||
IntegrationModel: models.Integration
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('returns null if anything goes wrong', async function () {
|
||||
modelStub.returns();
|
||||
|
||||
const key = await service.getFrontendKey();
|
||||
|
||||
sinon.assert.calledOnce(modelStub);
|
||||
assert.equal(key, null);
|
||||
});
|
||||
|
||||
it('returns the key from a model response', async function () {
|
||||
modelStub.returns(fakeModel);
|
||||
|
||||
const key = await service.getFrontendKey();
|
||||
|
||||
sinon.assert.calledOnce(modelStub);
|
||||
assert.equal(key, 'xyz');
|
||||
});
|
||||
|
||||
it('returns the key from cache the second time', async function () {
|
||||
modelStub.returns(fakeModel);
|
||||
|
||||
let key = await service.getFrontendKey();
|
||||
|
||||
sinon.assert.calledOnce(modelStub);
|
||||
assert.equal(key, 'xyz');
|
||||
|
||||
key = await service.getFrontendKey();
|
||||
sinon.assert.calledOnce(modelStub);
|
||||
assert.equal(key, 'xyz');
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
const models = require('../../../../../core/server/models');
|
||||
const assert = require('assert');
|
||||
|
||||
describe('Frontend Data Service', function () {
|
||||
let frontendDataService;
|
||||
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
||||
it('Provides expected public API', async function () {
|
||||
frontendDataService = require('../../../../../core/server/services/frontend-data-service');
|
||||
|
||||
assert.ok(frontendDataService.init);
|
||||
});
|
||||
|
||||
it('init gets an integration model', function () {
|
||||
frontendDataService = require('../../../../../core/server/services/frontend-data-service');
|
||||
let instance = frontendDataService.init();
|
||||
|
||||
assert.ok(instance.IntegrationModel);
|
||||
assert.equal(instance.frontendKey, null);
|
||||
});
|
||||
});
|
@ -800,6 +800,13 @@
|
||||
"description": "Internal Scheduler integration",
|
||||
"type": "internal",
|
||||
"api_keys": [{"type": "admin", "role": "Scheduler Integration"}]
|
||||
},
|
||||
{
|
||||
"slug": "ghost-internal-frontend",
|
||||
"name": "Ghost Internal Frontend",
|
||||
"description": "Internal frontend integration",
|
||||
"type": "internal",
|
||||
"api_keys": [{"type": "content"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user