mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
🎨 public config endpoint (#7631)
closes #7628 With this PR we expose a public configuration endpoint. When /ghost is requested, we don't load and render the configurations into the template anymore. Instead, Ghost-Admin can request the public configuration endpoint. * 🎨 make configuration endpoint public * 🔥 remove loading configurations in admin app - do not render them into the default html page * ✨ load client credentials in configuration endpoint - this is not a security issue, because we have exposed this information anyway before (by rendering them into the requested html page) * 🎨 extend existing configuration integration test * ✨ tests: add ghost-auth to data generator * ✨ add functional test * 🔥 remove type/value pattern * 🎨 do not return stringified JSON objects
This commit is contained in:
parent
e11e3a2444
commit
a55fb0bafe
6 changed files with 111 additions and 63 deletions
|
@ -1,8 +1,6 @@
|
||||||
var debug = require('debug')('ghost:admin:controller'),
|
var debug = require('debug')('ghost:admin:controller'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
Promise = require('bluebird'),
|
|
||||||
api = require('../api'),
|
api = require('../api'),
|
||||||
config = require('../config'),
|
|
||||||
logging = require('../logging'),
|
logging = require('../logging'),
|
||||||
updateCheck = require('../update-check'),
|
updateCheck = require('../update-check'),
|
||||||
i18n = require('../i18n');
|
i18n = require('../i18n');
|
||||||
|
@ -14,35 +12,6 @@ module.exports = function adminController(req, res) {
|
||||||
/*jslint unparam:true*/
|
/*jslint unparam:true*/
|
||||||
debug('index called');
|
debug('index called');
|
||||||
|
|
||||||
function renderIndex() {
|
|
||||||
var configuration,
|
|
||||||
fetch = {
|
|
||||||
configuration: api.configuration.read().then(function (res) { return res.configuration[0]; }),
|
|
||||||
client: api.clients.read({slug: 'ghost-admin'}).then(function (res) { return res.clients[0]; }),
|
|
||||||
ghostAuth: api.clients.read({slug: 'ghost-auth'})
|
|
||||||
.then(function (res) { return res.clients[0]; })
|
|
||||||
.catch(function () {
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
return Promise.props(fetch).then(function renderIndex(result) {
|
|
||||||
configuration = result.configuration;
|
|
||||||
|
|
||||||
configuration.clientId = {value: result.client.slug, type: 'string'};
|
|
||||||
configuration.clientSecret = {value: result.client.secret, type: 'string'};
|
|
||||||
|
|
||||||
if (result.ghostAuth && config.get('auth:type') === 'ghost') {
|
|
||||||
configuration.ghostAuthId = {value: result.ghostAuth.uuid, type: 'string'};
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('rendering default template');
|
|
||||||
res.render('default', {
|
|
||||||
configuration: configuration
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCheck().then(function then() {
|
updateCheck().then(function then() {
|
||||||
return updateCheck.showUpdateNotification();
|
return updateCheck.showUpdateNotification();
|
||||||
}).then(function then(updateVersion) {
|
}).then(function then(updateVersion) {
|
||||||
|
@ -64,6 +33,6 @@ module.exports = function adminController(req, res) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).finally(function noMatterWhat() {
|
}).finally(function noMatterWhat() {
|
||||||
renderIndex();
|
res.render('default');
|
||||||
}).catch(logging.logError);
|
}).catch(logging.logError);
|
||||||
};
|
};
|
||||||
|
|
|
@ -59,7 +59,7 @@ function apiRoutes() {
|
||||||
apiRouter.options('*', cors);
|
apiRouter.options('*', cors);
|
||||||
|
|
||||||
// ## Configuration
|
// ## Configuration
|
||||||
apiRouter.get('/configuration', authenticatePrivate, api.http(api.configuration.read));
|
apiRouter.get('/configuration', api.http(api.configuration.read));
|
||||||
apiRouter.get('/configuration/:key', authenticatePrivate, api.http(api.configuration.read));
|
apiRouter.get('/configuration/:key', authenticatePrivate, api.http(api.configuration.read));
|
||||||
apiRouter.get('/configuration/timezones', authenticatePrivate, api.http(api.configuration.read));
|
apiRouter.get('/configuration/timezones', authenticatePrivate, api.http(api.configuration.read));
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,11 @@
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
config = require('../config'),
|
config = require('../config'),
|
||||||
ghostVersion = require('../utils/ghost-version'),
|
ghostVersion = require('../utils/ghost-version'),
|
||||||
|
models = require('../models'),
|
||||||
Promise = require('bluebird'),
|
Promise = require('bluebird'),
|
||||||
|
|
||||||
configuration;
|
configuration;
|
||||||
|
|
||||||
function labsFlag(key) {
|
|
||||||
return {
|
|
||||||
value: (config[key] === true),
|
|
||||||
type: 'bool'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchAvailableTimezones() {
|
function fetchAvailableTimezones() {
|
||||||
var timezones = require('../data/timezones.json');
|
var timezones = require('../data/timezones.json');
|
||||||
return timezones;
|
return timezones;
|
||||||
|
@ -30,18 +24,22 @@ function getAboutConfig() {
|
||||||
|
|
||||||
function getBaseConfig() {
|
function getBaseConfig() {
|
||||||
return {
|
return {
|
||||||
fileStorage: {value: (config.fileStorage !== false), type: 'bool'},
|
fileStorage: config.get('fileStorage') !== false,
|
||||||
useGravatar: {value: !config.isPrivacyDisabled('useGravatar'), type: 'bool'},
|
useGravatar: !config.isPrivacyDisabled('useGravatar'),
|
||||||
publicAPI: labsFlag('publicAPI'),
|
publicAPI: config.get('publicAPI') === true,
|
||||||
blogUrl: {value: config.get('url').replace(/\/$/, ''), type: 'string'},
|
blogUrl: config.get('url').replace(/\/$/, ''),
|
||||||
blogTitle: {value: config.get('theme').title, type: 'string'},
|
blogTitle: config.get('theme').title,
|
||||||
routeKeywords: {value: JSON.stringify(config.get('routeKeywords')), type: 'json'}
|
routeKeywords: config.get('routeKeywords')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Configuration API Methods
|
* ## Configuration API Methods
|
||||||
*
|
*
|
||||||
|
* We need to load the client credentials dynamically.
|
||||||
|
* For example: on bootstrap ghost-auth get's created and if we load them here in parallel,
|
||||||
|
* it can happen that we won't get any client credentials or wrong credentials.
|
||||||
|
*
|
||||||
* **See:** [API Methods](index.js.html#api%20methods)
|
* **See:** [API Methods](index.js.html#api%20methods)
|
||||||
*/
|
*/
|
||||||
configuration = {
|
configuration = {
|
||||||
|
@ -54,9 +52,29 @@ configuration = {
|
||||||
*/
|
*/
|
||||||
read: function read(options) {
|
read: function read(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
var ops = {};
|
||||||
|
|
||||||
if (!options.key) {
|
if (!options.key) {
|
||||||
return Promise.resolve({configuration: [getBaseConfig()]});
|
ops.ghostAdmin = models.Client.findOne({slug: 'ghost-admin'});
|
||||||
|
|
||||||
|
if (config.get('auth:type') === 'ghost') {
|
||||||
|
ops.ghostAuth = models.Client.findOne({slug: 'ghost-auth'});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.props(ops)
|
||||||
|
.then(function (result) {
|
||||||
|
var configuration = getBaseConfig();
|
||||||
|
|
||||||
|
configuration.clientId = result.ghostAdmin.get('slug');
|
||||||
|
configuration.clientSecret = result.ghostAdmin.get('secret');
|
||||||
|
|
||||||
|
if (result.ghostAuth) {
|
||||||
|
configuration.ghostAuthId = result.ghostAuth.get('uuid');
|
||||||
|
configuration.ghostAuthUrl = config.get('auth:url');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {configuration: [configuration]};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.key === 'about') {
|
if (options.key === 'about') {
|
||||||
|
|
41
core/test/functional/routes/api/configuration_spec.js
Normal file
41
core/test/functional/routes/api/configuration_spec.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
var testUtils = require('../../../utils'),
|
||||||
|
should = require('should'),
|
||||||
|
supertest = require('supertest'),
|
||||||
|
ghost = testUtils.startGhost,
|
||||||
|
request;
|
||||||
|
|
||||||
|
describe('Configuration API', function () {
|
||||||
|
var accesstoken = '';
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
// starting ghost automatically populates the db
|
||||||
|
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||||
|
ghost().then(function (ghostServer) {
|
||||||
|
request = supertest.agent(ghostServer.rootApp);
|
||||||
|
}).then(function () {
|
||||||
|
return testUtils.doAuth(request, 'posts');
|
||||||
|
}).then(function (token) {
|
||||||
|
accesstoken = token;
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
testUtils.clearData().then(function () {
|
||||||
|
done();
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('success', function () {
|
||||||
|
it('can retrieve public configuration', function (done) {
|
||||||
|
request.get(testUtils.API.getApiQuery('configuration/'))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(200)
|
||||||
|
.end(function (err, res) {
|
||||||
|
should.exist(res.body.configuration);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,18 +1,25 @@
|
||||||
var testUtils = require('../../utils'),
|
var testUtils = require('../../utils'),
|
||||||
should = require('should'),
|
configUtils = require('../../utils/configUtils'),
|
||||||
rewire = require('rewire'),
|
should = require('should'),
|
||||||
|
rewire = require('rewire'),
|
||||||
|
|
||||||
// Stuff we are testing
|
// Stuff we are testing
|
||||||
ConfigurationAPI = rewire('../../../server/api/configuration');
|
ConfigurationAPI = rewire('../../../server/api/configuration');
|
||||||
|
|
||||||
describe('Configuration API', function () {
|
describe('Configuration API', function () {
|
||||||
// Keep the DB clean
|
// Keep the DB clean
|
||||||
before(testUtils.teardown);
|
before(testUtils.teardown);
|
||||||
afterEach(testUtils.teardown);
|
beforeEach(testUtils.setup('clients'));
|
||||||
|
afterEach(function () {
|
||||||
|
configUtils.restore();
|
||||||
|
return testUtils.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
should.exist(ConfigurationAPI);
|
should.exist(ConfigurationAPI);
|
||||||
|
|
||||||
it('can read basic config and get all expected properties', function (done) {
|
it('can read basic config and get all expected properties', function (done) {
|
||||||
|
configUtils.set('auth:type', 'ghost');
|
||||||
|
|
||||||
ConfigurationAPI.read().then(function (response) {
|
ConfigurationAPI.read().then(function (response) {
|
||||||
var props;
|
var props;
|
||||||
|
|
||||||
|
@ -21,17 +28,29 @@ describe('Configuration API', function () {
|
||||||
response.configuration.should.be.an.Array().with.lengthOf(1);
|
response.configuration.should.be.an.Array().with.lengthOf(1);
|
||||||
props = response.configuration[0];
|
props = response.configuration[0];
|
||||||
|
|
||||||
// Check the structure
|
props.blogUrl.should.eql('http://127.0.0.1:2369');
|
||||||
props.should.have.property('blogUrl').which.is.an.Object().with.properties('type', 'value');
|
props.routeKeywords.should.eql({
|
||||||
props.should.have.property('blogTitle').which.is.an.Object().with.properties('type', 'value');
|
tag: 'tag',
|
||||||
props.should.have.property('routeKeywords').which.is.an.Object().with.properties('type', 'value');
|
author: 'author',
|
||||||
props.should.have.property('fileStorage').which.is.an.Object().with.properties('type', 'value');
|
page: 'page',
|
||||||
props.should.have.property('useGravatar').which.is.an.Object().with.properties('type', 'value');
|
preview: 'p',
|
||||||
props.should.have.property('publicAPI').which.is.an.Object().with.properties('type', 'value');
|
private: 'private',
|
||||||
|
subscribe: 'subscribe',
|
||||||
|
amp: 'amp'
|
||||||
|
});
|
||||||
|
|
||||||
// Check a few values
|
props.fileStorage.should.eql(true);
|
||||||
props.blogUrl.should.have.property('value', 'http://127.0.0.1:2369');
|
props.useGravatar.should.eql(true);
|
||||||
props.fileStorage.should.have.property('value', true);
|
props.publicAPI.should.eql(false);
|
||||||
|
props.clientId.should.eql('ghost-admin');
|
||||||
|
props.clientSecret.should.eql('not_available');
|
||||||
|
props.ghostAuthUrl.should.eql('http://devauth.ghost.org:8080');
|
||||||
|
|
||||||
|
// value not available, because settings API was not called yet
|
||||||
|
props.hasOwnProperty('blogTitle').should.eql(true);
|
||||||
|
|
||||||
|
// uuid
|
||||||
|
props.hasOwnProperty('ghostAuthId').should.eql(true);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}).catch(done);
|
}).catch(done);
|
||||||
|
|
|
@ -442,7 +442,8 @@ DataGenerator.forKnex = (function () {
|
||||||
|
|
||||||
clients = [
|
clients = [
|
||||||
createClient({name: 'Ghost Admin', slug: 'ghost-admin', type: 'ua'}),
|
createClient({name: 'Ghost Admin', slug: 'ghost-admin', type: 'ua'}),
|
||||||
createClient({name: 'Ghost Scheduler', slug: 'ghost-scheduler', type: 'web'})
|
createClient({name: 'Ghost Scheduler', slug: 'ghost-scheduler', type: 'web'}),
|
||||||
|
createClient({name: 'Ghost Auth', slug: 'ghost-auth', type: 'web'})
|
||||||
];
|
];
|
||||||
|
|
||||||
roles_users = [
|
roles_users = [
|
||||||
|
|
Loading…
Add table
Reference in a new issue