diff --git a/core/server/api/app.js b/core/server/api/app.js index c05c569b11..9b7815adf4 100644 --- a/core/server/api/app.js +++ b/core/server/api/app.js @@ -30,18 +30,28 @@ var debug = require('debug')('ghost:api'), // @TODO find a more appy way to do this! labs = require('../middleware/labs'), - // @TODO find a better way to bundle these authentication packages - // Authentication for public endpoints + /** + * Authentication for public endpoints + * @TODO find a better way to bundle these authentication packages + * + * IMPORTANT + * - cors middleware MUST happen before pretty urls, because otherwise cors header can get lost + * - cors middleware MUST happen after authenticateClient, because authenticateClient reads the trusted domains + */ authenticatePublic = [ auth.authenticate.authenticateClient, auth.authenticate.authenticateUser, - auth.authorize.requiresAuthorizedUserPublicAPI + auth.authorize.requiresAuthorizedUserPublicAPI, + cors, + prettyURLs ], // Require user for private endpoints authenticatePrivate = [ auth.authenticate.authenticateClient, auth.authenticate.authenticateUser, - auth.authorize.requiresAuthorizedUser + auth.authorize.requiresAuthorizedUser, + cors, + prettyURLs ]; // @TODO refactor/clean this up - how do we want the routing to work long term? @@ -230,8 +240,6 @@ module.exports = function setupApiApp() { apiApp.use(bodyParser.json({limit: '1mb'})); apiApp.use(bodyParser.urlencoded({extended: true, limit: '1mb'})); - apiApp.use(cors); - // send 503 json response in case of maintenance apiApp.use(maintenance); @@ -239,10 +247,6 @@ module.exports = function setupApiApp() { // must happen AFTER asset loading and BEFORE routing apiApp.use(urlRedirects); - // Add in all trailing slashes & remove uppercase - // must happen AFTER asset loading and BEFORE routing - apiApp.use(prettyURLs); - // Check version matches for API requests, depends on res.locals.safeVersion being set // Therefore must come after themeHandler.ghostLocals, for now apiApp.use(versionMatch); diff --git a/core/test/functional/routes/api/public_api_spec.js b/core/test/functional/routes/api/public_api_spec.js index 004eee9f73..94b09ecbeb 100644 --- a/core/test/functional/routes/api/public_api_spec.js +++ b/core/test/functional/routes/api/public_api_spec.js @@ -22,7 +22,7 @@ describe('Public API', function () { }).then(function () { request = supertest.agent(config.get('url')); }).then(function () { - return testUtils.doAuth(request, 'posts', 'tags'); + return testUtils.doAuth(request, 'posts', 'tags', 'client:trusted-domain'); }).then(function (token) { // enable public API request.put(testUtils.API.getApiQuery('settings/')) @@ -58,7 +58,37 @@ describe('Public API', function () { return done(err); } + res.headers.vary.should.eql('Origin, Accept-Encoding'); + should.exist(res.headers['access-control-allow-origin']); should.not.exist(res.headers['x-cache-invalidate']); + + var jsonResponse = res.body; + should.exist(jsonResponse.posts); + testUtils.API.checkResponse(jsonResponse, 'posts'); + jsonResponse.posts.should.have.length(5); + testUtils.API.checkResponse(jsonResponse.posts[0], 'post'); + testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination'); + _.isBoolean(jsonResponse.posts[0].featured).should.eql(true); + _.isBoolean(jsonResponse.posts[0].page).should.eql(true); + done(); + }); + }); + + it('browse posts from different origin', function (done) { + request.get(testUtils.API.getApiQuery('posts/?client_id=ghost-test&client_secret=not_available')) + .set('Origin', 'https://example.com') + .expect('Content-Type', /json/) + .expect('Cache-Control', testUtils.cacheRules.private) + .expect(200) + .end(function (err, res) { + if (err) { + return done(err); + } + + res.headers.vary.should.eql('Origin, Accept-Encoding'); + should.exist(res.headers['access-control-allow-origin']); + should.not.exist(res.headers['x-cache-invalidate']); + var jsonResponse = res.body; should.exist(jsonResponse.posts); testUtils.API.checkResponse(jsonResponse, 'posts'); diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index d60ca91e23..a81d027e54 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -406,6 +406,7 @@ DataGenerator.forKnex = (function () { secret: 'not_available', redirection_uri: 'http://localhost:9999', client_uri: 'http://localhost:9000', + slug: 'client', name: 'client', type: 'ua', status: 'enabled' @@ -480,6 +481,16 @@ DataGenerator.forKnex = (function () { }); } + function createTrustedDomain(overrides) { + var newObj = _.cloneDeep(overrides); + + return _.defaults(newObj, { + id: ObjectId.generate(), + client_id: clients[0].id, + trusted_domain: 'https://example.com' + }); + } + posts = [ createPost(DataGenerator.Content.posts[0]), createPost(DataGenerator.Content.posts[1]), @@ -600,6 +611,7 @@ DataGenerator.forKnex = (function () { createToken: createToken, createSubscriber: createBasic, createInvite: createInvite, + createTrustedDomain: createTrustedDomain, invites: invites, posts: posts, diff --git a/core/test/utils/index.js b/core/test/utils/index.js index 77ecdd937f..3a4d02d5eb 100644 --- a/core/test/utils/index.js +++ b/core/test/utils/index.js @@ -387,6 +387,17 @@ fixtures = { return db.knex('clients').insert(DataGenerator.forKnex.clients); }, + insertClientWithTrustedDomain: function insertClientWithTrustedDomain() { + var client = DataGenerator.forKnex.createClient({slug: 'ghost-test'}); + + return db.knex('clients') + .insert(client) + .then(function () { + return db.knex('client_trusted_domains') + .insert(DataGenerator.forKnex.createTrustedDomain({client_id: client.id})); + }); + }, + insertAccessToken: function insertAccessToken(override) { return db.knex('accesstokens').insert(DataGenerator.forKnex.createToken(override)); }, @@ -447,6 +458,7 @@ toDoList = { return function permissionsForObj() { return fixtures.permissionsFor(obj); }; }, clients: function insertClients() { return fixtures.insertClients(); }, + 'client:trusted-domain': function insertClients() { return fixtures.insertClientWithTrustedDomain(); }, filter: function createFilterParamFixtures() { return filterData(DataGenerator); }, invites: function insertInvites() { return fixtures.insertInvites(); }, themes: function loadThemes() { return themes.loadAll(); }