0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Restructure Configuration API endpoint

refs #6421, #6525

- The configuration API endpoint was a bit of an animal:
   - It's used currently in two ways, once for general config, another for the about page.
   - These two things are different, and would require different permissions in future.
   - There was also both a browse and a read version, even though only browse was used.
   - The response from the browse was being artificially turned into many objects, when its really just one with multiple keys
- The new version treats each type of config as a different single object with several keys
- The new version therefore only has a 'read' request
- A basic read request with no key will return basic config that any client would need
- A read request with the about key returns the about config
- A read request with a different key could therefore return some other config
This commit is contained in:
Hannah Wolfe 2016-02-19 18:18:14 +00:00
parent 72cbda3646
commit ed16998461
7 changed files with 82 additions and 103 deletions

View file

@ -31,13 +31,13 @@
<meta name="msapplication-square150x150logo" content="{{asset "img/medium.png" ghost="true"}}" />
<meta name="msapplication-square310x310logo" content="{{asset "img/large.png" ghost="true"}}" />
{{#each configuration}}
<meta name="env-{{this.key}}" content="{{this.value}}" data-type="{{this.type}}" />
{{#each configuration as |config key|}}
<meta name="env-{{key}}" content="{{config.value}}" data-type="{{config.type}}" />
{{/each}}
{{#unless skip_google_fonts}}
{{#if configuration.useGoogleFonts.value}}
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:400,300,700" />
{{/unless}}
{{/if}}
<link rel="stylesheet" href="{{asset "vendor.css" ghost="true" minifyInProduction="true"}}" />
<link rel="stylesheet" href="{{asset "ghost.css" ghost="true" minifyInProduction="true"}}" />

View file

@ -18,7 +18,7 @@ export default AuthenticatedRoute.extend(styleBody, {
model() {
let cachedConfig = this.get('cachedConfig');
let configUrl = this.get('ghostPaths.url').api('configuration');
let configUrl = this.get('ghostPaths.url').api('configuration', 'about');
if (cachedConfig) {
return cachedConfig;
@ -26,12 +26,8 @@ export default AuthenticatedRoute.extend(styleBody, {
return this.get('ajax').request(configUrl)
.then((configurationResponse) => {
let configKeyValues = configurationResponse.configuration;
let [cachedConfig] = configurationResponse.configuration;
cachedConfig = {};
configKeyValues.forEach((configKeyValue) => {
cachedConfig[configKeyValue.key] = configKeyValue.value;
});
this.set('cachedConfig', cachedConfig);
return cachedConfig;

View file

@ -2,9 +2,7 @@
// RESTful API for browsing the configuration
var _ = require('lodash'),
config = require('../config'),
errors = require('../errors'),
Promise = require('bluebird'),
i18n = require('../i18n'),
configuration;
@ -15,28 +13,24 @@ function labsFlag(key) {
};
}
function getValidKeys() {
var validKeys = {
fileStorage: {value: (config.fileStorage !== false), type: 'bool'},
publicAPI: labsFlag('publicAPI'),
apps: {value: (config.apps === true), type: 'bool'},
version: {value: config.ghostVersion, type: 'string'},
environment: process.env.NODE_ENV,
database: config.database.client,
mail: _.isObject(config.mail) ? config.mail.transport : '',
blogUrl: {value: config.url.replace(/\/$/, ''), type: 'string'},
blogTitle: {value: config.theme.title, type: 'string'},
routeKeywords: {value: JSON.stringify(config.routeKeywords), type: 'json'}
};
return validKeys;
function getAboutConfig() {
return {
version: config.ghostVersion,
environment: process.env.NODE_ENV,
database: config.database.client,
mail: _.isObject(config.mail) ? config.mail.transport : ''
};
}
function formatConfigurationObject(val, key) {
function getBaseConfig() {
return {
key: key,
value: (_.isObject(val) && _.has(val, 'value')) ? val.value : val,
type: _.isObject(val) ? (val.type || null) : null
fileStorage: {value: (config.fileStorage !== false), type: 'bool'},
useGoogleFonts: {value: !config.isPrivacyDisabled('useGoogleFonts'), type: 'bool'},
useGravatar: {value: !config.isPrivacyDisabled('useGravatar'), type: 'bool'},
publicAPI: labsFlag('publicAPI'),
blogUrl: {value: config.url.replace(/\/$/, ''), type: 'string'},
blogTitle: {value: config.theme.title, type: 'string'},
routeKeywords: {value: JSON.stringify(config.routeKeywords), type: 'json'}
};
}
@ -48,26 +42,23 @@ function formatConfigurationObject(val, key) {
configuration = {
/**
* ### Browse
* Fetch all configuration keys
* @returns {Promise(Configurations)}
*/
browse: function browse() {
return Promise.resolve({configuration: _.map(getValidKeys(), formatConfigurationObject)});
},
/**
* ### Read
*
* Always returns {configuration: []}
* Sometimes the array contains configuration items
* @param {Object} options
* @returns {Promise<Object>}
*/
read: function read(options) {
var data = getValidKeys();
options = options || {};
if (_.has(data, options.key)) {
return Promise.resolve({configuration: [formatConfigurationObject(data[options.key], options.key)]});
} else {
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.configuration.invalidKey')));
if (!options.key) {
return Promise.resolve({configuration: [getBaseConfig()]});
}
if (options.key === 'about') {
return Promise.resolve({configuration: [getAboutConfig()]});
}
return Promise.resolve({configuration: []});
}
};

View file

@ -1,8 +1,8 @@
var _ = require('lodash'),
Promise = require('bluebird'),
api = require('../api'),
errors = require('../errors'),
updateCheck = require('../update-check'),
config = require('../config'),
i18n = require('../i18n'),
adminControllers;
@ -14,22 +14,20 @@ adminControllers = {
/*jslint unparam:true*/
function renderIndex() {
var configuration;
return api.configuration.browse().then(function then(data) {
configuration = data.configuration;
}).then(function getAPIClient() {
return api.clients.read({slug: 'ghost-admin'});
}).then(function renderIndex(adminClient) {
configuration.push({key: 'clientId', value: adminClient.clients[0].slug, type: 'string'});
configuration.push({key: 'clientSecret', value: adminClient.clients[0].secret, type: 'string'});
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]; })
};
var apiConfig = _.omit(configuration, function omit(value) {
return _.contains(['environment', 'database', 'mail', 'version'], value.key);
});
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'};
res.render('default', {
skip_google_fonts: config.isPrivacyDisabled('useGoogleFonts'),
configuration: apiConfig
configuration: configuration
});
});
}

View file

@ -22,7 +22,7 @@ apiRoutes = function apiRoutes(middleware) {
router.del = router.delete;
// ## Configuration
router.get('/configuration', authenticatePrivate, api.http(api.configuration.browse));
router.get('/configuration', authenticatePrivate, api.http(api.configuration.read));
router.get('/configuration/:key', authenticatePrivate, api.http(api.configuration.read));
// ## Posts

View file

@ -1,68 +1,63 @@
/*globals describe, before, afterEach, it */
var testUtils = require('../../utils'),
should = require('should'),
rewire = require('rewire'),
_ = require('lodash'),
config = rewire('../../../server/config'),
// Stuff we are testing
ConfigurationAPI = rewire('../../../server/api/configuration');
describe('Configuration API', function () {
var newConfig = {
fileStorage: true,
apps: true,
version: '0.5.0',
environment: process.env.NODE_ENV,
database: {
client: 'mysql'
},
mail: {
transport: 'SMTP'
},
blogUrl: 'http://local.tryghost.org'
};
// Keep the DB clean
before(testUtils.teardown);
afterEach(testUtils.teardown);
should.exist(ConfigurationAPI);
it('can browse config', function (done) {
var updatedConfig = _.extend({}, config, newConfig);
config.set(updatedConfig);
ConfigurationAPI.__set__('config', updatedConfig);
it('can read basic config and get all expected properties', function (done) {
ConfigurationAPI.read().then(function (response) {
var props;
ConfigurationAPI.browse(testUtils.context.owner).then(function (response) {
should.exist(response);
should.exist(response.configuration);
testUtils.API.checkResponse(response.configuration[0], 'configuration');
/*jshint unused:false */
done();
}).catch(function (error) {
console.log(JSON.stringify(error));
response.configuration.should.be.an.Array().with.lengthOf(1);
props = response.configuration[0];
// Check the structure
props.should.have.property('blogUrl').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('blogTitle').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('routeKeywords').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('fileStorage').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('useGoogleFonts').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('useGravatar').which.is.an.Object().with.properties('type', 'value');
props.should.have.property('publicAPI').which.is.an.Object().with.properties('type', 'value');
// Check a few values
props.blogUrl.should.have.property('value', 'http://127.0.0.1:2369');
props.fileStorage.should.have.property('value', true);
done();
}).catch(done);
});
it('can read config', function (done) {
var updatedConfig = _.extend({}, config, newConfig);
config.set(updatedConfig);
ConfigurationAPI.__set__('config', updatedConfig);
it('can read about config and get all expected properties', function (done) {
ConfigurationAPI.read({key: 'about'}).then(function (response) {
var props;
ConfigurationAPI.read(_.extend({}, testUtils.context.owner, {key: 'database'})).then(function (response) {
should.exist(response);
should.exist(response.configuration);
testUtils.API.checkResponse(response.configuration[0], 'configuration');
response.configuration[0].key.should.equal('database');
response.configuration[0].value.should.equal('mysql');
response.configuration[0].type.should.be.null();
/*jshint unused:false */
done();
}).catch(function (error) {
console.log(JSON.stringify(error));
response.configuration.should.be.an.Array().with.lengthOf(1);
props = response.configuration[0];
// Check the structure
props.should.have.property('version').which.is.a.String();
props.should.have.property('environment').which.is.a.String();
props.should.have.property('database').which.is.a.String();
props.should.have.property('mail').which.is.a.String();
// Check a few values
props.environment.should.match(/^testing/);
props.version.should.eql(require('../../../../package.json').version);
done();
}).catch(done);
});

View file

@ -9,7 +9,6 @@ var _ = require('lodash'),
protocol = 'http://',
expectedProperties = {
// API top level
configuration: ['key', 'value', 'type'],
posts: ['posts', 'meta'],
tags: ['tags', 'meta'],
users: ['users', 'meta'],