diff --git a/ghost/core/core/server/api/endpoints/announcements.js b/ghost/core/core/server/api/endpoints/announcements.js new file mode 100644 index 0000000000..903cda8430 --- /dev/null +++ b/ghost/core/core/server/api/endpoints/announcements.js @@ -0,0 +1,12 @@ +const announcementBarSettings = require('../../services/announcement-bar-service'); + +module.exports = { + docName: 'announcement', + + browse: { + permissions: true, + query(frame) { + return announcementBarSettings.getAnnouncementSettings(frame.options.context?.member); + } + } +}; diff --git a/ghost/core/core/server/api/endpoints/index.js b/ghost/core/core/server/api/endpoints/index.js index 298a4b5ecd..3f598eb29d 100644 --- a/ghost/core/core/server/api/endpoints/index.js +++ b/ghost/core/core/server/api/endpoints/index.js @@ -77,6 +77,10 @@ module.exports = { return apiFramework.pipeline(require('./settings'), localUtils); }, + get announcements() { + return apiFramework.pipeline(require('./announcements'), localUtils); + }, + get membersStripeConnect() { return apiFramework.pipeline(require('./members-stripe-connect'), localUtils); }, @@ -239,5 +243,5 @@ module.exports = { get feedbackMembers() { return apiFramework.pipeline(require('./feedback-members'), localUtils, 'members'); - } + } }; diff --git a/ghost/core/core/server/api/endpoints/settings-public.js b/ghost/core/core/server/api/endpoints/settings-public.js index f337c040cb..27ea7bb504 100644 --- a/ghost/core/core/server/api/endpoints/settings-public.js +++ b/ghost/core/core/server/api/endpoints/settings-public.js @@ -1,25 +1,17 @@ const settingsCache = require('../../../shared/settings-cache'); const urlUtils = require('../../../shared/url-utils'); const ghostVersion = require('@tryghost/version'); -const announcementBarSettings = require('../../services/announcement-bar-service'); -const labs = require('../../../shared/labs'); module.exports = { docName: 'settings', browse: { permissions: true, - query(frame) { - let announcementSettings; - if (labs.isSet('announcementBar')) { - announcementSettings = announcementBarSettings.getAnnouncementSettings(frame.options.context?.member); - } - + query() { // @TODO: decouple settings cache from API knowledge // The controller fetches models (or cached models) and the API frame for the target API version formats the response. return Object.assign({}, - settingsCache.getPublic(), - announcementSettings, { + settingsCache.getPublic(), { url: urlUtils.urlFor('home', true), version: ghostVersion.safe } diff --git a/ghost/core/core/server/web/announcement/index.js b/ghost/core/core/server/web/announcement/index.js new file mode 100644 index 0000000000..3b3a4503eb --- /dev/null +++ b/ghost/core/core/server/web/announcement/index.js @@ -0,0 +1 @@ +module.exports = require('./routes'); diff --git a/ghost/core/core/server/web/announcement/routes.js b/ghost/core/core/server/web/announcement/routes.js new file mode 100644 index 0000000000..9b97b2358a --- /dev/null +++ b/ghost/core/core/server/web/announcement/routes.js @@ -0,0 +1,15 @@ +const express = require('../../../shared/express'); +const api = require('../../api').endpoints; +const {http} = require('@tryghost/api-framework'); +const shared = require('../shared'); + +module.exports = function apiRoutes() { + const router = express.Router('announcements'); + + // shouldn't be cached as it depends on member's context + router.use(shared.middleware.cacheControl('private')); + + router.get('/', http(api.announcements.browse)); + + return router; +}; diff --git a/ghost/core/core/server/web/members/app.js b/ghost/core/core/server/web/members/app.js index 8fdb483c0c..6f558d73be 100644 --- a/ghost/core/core/server/web/members/app.js +++ b/ghost/core/core/server/web/members/app.js @@ -14,6 +14,7 @@ const {http} = require('@tryghost/api-framework'); const api = require('../../api').endpoints; const commentRouter = require('../comments'); +const announcementRouter = require('../announcement'); module.exports = function setupMembersApp() { debug('Members App setup start'); @@ -79,6 +80,14 @@ module.exports = function setupMembersApp() { http(api.feedbackMembers.add) ); + // Announcement + membersApp.use( + '/api/announcement', + labs.enabledMiddleware('announcementBar'), + middleware.loadMemberSession, + announcementRouter() + ); + // API error handling membersApp.use('/api', errorHandler.resourceNotFound); membersApp.use('/api', errorHandler.handleJSONResponse(sentry)); diff --git a/ghost/core/test/e2e-api/members/__snapshots__/announcement.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/announcement.test.js.snap new file mode 100644 index 0000000000..a4423bda57 --- /dev/null +++ b/ghost/core/test/e2e-api/members/__snapshots__/announcement.test.js.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Announcement Can read announcement data 1: [body] 1`] = ` +Object { + "announcement": Array [ + Object {}, + ], +} +`; + +exports[`Announcement Can read announcement data 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "*", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "21", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Announcement Can read announcement endpoint 1: [body] 1`] = ` +Object { + "announcement": Array [ + Object {}, + ], +} +`; + +exports[`Announcement Can read announcement endpoint 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "*", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "21", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Announcement Can read announcement when it is present in announcement data 1: [body] 1`] = ` +Object { + "announcement": Array [ + Object { + "announcement": "

Test announcement

", + "announcement_background": "dark", + }, + ], +} +`; + +exports[`Announcement Can read announcement when it is present in announcement data 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "*", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "95", + "content-type": "application/json; charset=utf-8", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Encoding", + "x-powered-by": "Express", +} +`; diff --git a/ghost/core/test/e2e-api/members/announcement.test.js b/ghost/core/test/e2e-api/members/announcement.test.js new file mode 100644 index 0000000000..c24630cf18 --- /dev/null +++ b/ghost/core/test/e2e-api/members/announcement.test.js @@ -0,0 +1,41 @@ +const {agentProvider, mockManager, fixtureManager, matchers, configUtils} = require('../../utils/e2e-framework'); +const {anyEtag} = matchers; +const settingsCache = require('../../../core/shared/settings-cache'); + +describe('Announcement', function () { + let membersAgent; + + before(async function () { + membersAgent = await agentProvider.getMembersAPIAgent(); + + await fixtureManager.init('members'); + }); + + afterEach(async function () { + await configUtils.restore(); + mockManager.restore(); + }); + + it('Can read announcement endpoint', async function () { + await membersAgent + .get(`/api/announcement/`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(); + }); + + it('Can read announcement when it is present in announcement data', async function () { + settingsCache.set('announcement_content', {value: '

Test announcement

'}); + settingsCache.set('announcement_visibility', {value: ['visitors']}); + + await membersAgent + .get(`/api/announcement/`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot(); + }); +});