From 74280cfbeae70d1a21bc1a75873302df528d5d05 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 13 Aug 2021 12:09:11 +0400 Subject: [PATCH] Added "email post" frontend routing refs https://github.com/TryGhost/Team/issues/953 - Emails posts should be not explorable by the rest of the frontend similarly to the draft or scheduled posts. Email posts should also keep the content gating, so that specific parts of content can still be gated based on the post's visibility setup - A separate frontend router was chosen to implement this part of the system instead of a moutable express app due to increased complexity to introduce the latter approach. - All "sent" email-only posts will be accessible through the `/email/:slug/` route --- core/frontend/services/routing/EmailRouter.js | 50 ++++++++++++++ .../services/routing/PreviewRouter.js | 7 -- core/frontend/services/routing/bootstrap.js | 8 +++ .../services/routing/config/canary.js | 7 ++ core/frontend/services/routing/config/v4.js | 7 ++ .../routing/controllers/email-post.js | 65 +++++++++++++++++++ .../services/routing/controllers/index.js | 4 ++ test/regression/site/email_routes.test.js | 23 ++++--- 8 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 core/frontend/services/routing/EmailRouter.js create mode 100644 core/frontend/services/routing/controllers/email-post.js diff --git a/core/frontend/services/routing/EmailRouter.js b/core/frontend/services/routing/EmailRouter.js new file mode 100644 index 0000000000..15ab83d426 --- /dev/null +++ b/core/frontend/services/routing/EmailRouter.js @@ -0,0 +1,50 @@ +const ParentRouter = require('./ParentRouter'); +const urlUtils = require('../../../shared/url-utils'); +const controllers = require('./controllers'); + +/** + * @description Preview Router. + */ +class EmailRouter extends ParentRouter { + constructor(RESOURCE_CONFIG) { + super('PreviewRouter'); + + this.RESOURCE_CONFIG = RESOURCE_CONFIG.QUERY.email; + + // @NOTE: hardcoded, not configureable + this.route = {value: '/email/'}; + + this._registerRoutes(); + } + + /** + * @description Register all routes of this router. + * @private + */ + _registerRoutes() { + // REGISTER: prepare context + this.router().use(this._prepareContext.bind(this)); + + // REGISTER: actual email route + this.mountRoute(urlUtils.urlJoin(this.route.value, ':slug', ':options?'), controllers.email); + } + + /** + * @description Prepare context for further middlewares/controllers. + * @param {Object} req + * @param {Object} res + * @param {Function} next + * @private + */ + _prepareContext(req, res, next) { + res.routerOptions = { + type: 'entry', + query: this.RESOURCE_CONFIG, + context: ['emailPost'] + }; + + next(); + } +} + +module.exports = EmailRouter; diff --git a/core/frontend/services/routing/PreviewRouter.js b/core/frontend/services/routing/PreviewRouter.js index b795e298cb..5970b6d134 100644 --- a/core/frontend/services/routing/PreviewRouter.js +++ b/core/frontend/services/routing/PreviewRouter.js @@ -1,6 +1,5 @@ const ParentRouter = require('./ParentRouter'); const urlUtils = require('../../../shared/url-utils'); -const labs = require('../../../shared/labs'); const controllers = require('./controllers'); /** @@ -28,12 +27,6 @@ class PreviewRouter extends ParentRouter { // REGISTER: actual preview route this.mountRoute(urlUtils.urlJoin(this.route.value, ':uuid', ':options?'), controllers.preview); - - // NOTE: temporary hack aliasing /email/ route to /p/ preview route - // /email/ will become it's own Router once the feature enters beta stage - if (labs.isSet('emailOnlyPosts')) { - this.mountRoute(urlUtils.urlJoin('/email/', ':uuid', ':options?'), controllers.preview); - } } /** diff --git a/core/frontend/services/routing/bootstrap.js b/core/frontend/services/routing/bootstrap.js index f0eab8d0b5..7744f82a4b 100644 --- a/core/frontend/services/routing/bootstrap.js +++ b/core/frontend/services/routing/bootstrap.js @@ -6,8 +6,10 @@ const CollectionRouter = require('./CollectionRouter'); const TaxonomyRouter = require('./TaxonomyRouter'); const PreviewRouter = require('./PreviewRouter'); const ParentRouter = require('./ParentRouter'); +const EmailRouter = require('./EmailRouter'); const UnsubscribeRouter = require('./UnsubscribeRouter'); +const labs = require('../../../shared/labs'); // This emits its own routing events const events = require('../../../server/lib/common/events'); @@ -71,6 +73,12 @@ module.exports.start = (apiVersion, routerSettings) => { siteRouter.mountRouter(unsubscribeRouter.router()); registry.setRouter('unsubscribeRouter', unsubscribeRouter); + if (labs.isSet('emailOnlyPosts')) { + const emailRouter = new EmailRouter(RESOURCE_CONFIG); + siteRouter.mountRouter(emailRouter.router()); + registry.setRouter('emailRouter', emailRouter); + } + const previewRouter = new PreviewRouter(RESOURCE_CONFIG); siteRouter.mountRouter(previewRouter.router()); registry.setRouter('previewRouter', previewRouter); diff --git a/core/frontend/services/routing/config/canary.js b/core/frontend/services/routing/config/canary.js index f72e92297b..0c4ffbb9ae 100644 --- a/core/frontend/services/routing/config/canary.js +++ b/core/frontend/services/routing/config/canary.js @@ -36,6 +36,13 @@ module.exports.QUERY = { preview: { controller: 'preview', resource: 'preview' + }, + email: { + controller: 'emailPost', + resource: 'email_posts', + options: { + slug: '%s' + } } }; diff --git a/core/frontend/services/routing/config/v4.js b/core/frontend/services/routing/config/v4.js index f72e92297b..0c4ffbb9ae 100644 --- a/core/frontend/services/routing/config/v4.js +++ b/core/frontend/services/routing/config/v4.js @@ -36,6 +36,13 @@ module.exports.QUERY = { preview: { controller: 'preview', resource: 'preview' + }, + email: { + controller: 'emailPost', + resource: 'email_posts', + options: { + slug: '%s' + } } }; diff --git a/core/frontend/services/routing/controllers/email-post.js b/core/frontend/services/routing/controllers/email-post.js new file mode 100644 index 0000000000..77d32ec6da --- /dev/null +++ b/core/frontend/services/routing/controllers/email-post.js @@ -0,0 +1,65 @@ +const debug = require('@tryghost/debug')('services:routing:controllers:emailpost'); +const config = require('../../../../shared/config'); +const urlService = require('../../url'); +const urlUtils = require('../../../../shared/url-utils'); +const helpers = require('../helpers'); + +/** + * @description Email Post Controller. + * @param {Object} req + * @param {Object} res + * @param {Function} next + * @returns {Promise} + */ +module.exports = function emailPostController(req, res, next) { + debug('emailPostController'); + + const api = require('../../proxy').api[res.locals.apiVersion]; + + const params = { + slug: req.params.slug, + include: 'authors,tags', + context: { + member: res.locals.member + } + }; + + return api[res.routerOptions.query.controller] + .read(params) + .then(function then(result) { + const post = result[res.routerOptions.query.resource][0]; + + if (!post) { + return next(); + } + + if (req.params.options && req.params.options.toLowerCase() === 'edit') { + // CASE: last param of the url is /edit but admin redirects are disabled + if (!config.get('admin:redirects')) { + return next(); + } + + // CASE: last param of the url is /edit, redirect to admin + // NOTE: only 'post' resources support email-only mode + return urlUtils.redirectToAdmin(302, res, `/#/editor/post/${post.id}`); + } else if (req.params.options) { + // CASE: unknown options param detected, ignore + return next(); + } + + if (post.status === 'published') { + return urlUtils.redirect301(res, urlService.getUrlByResourceId(post.id, {withSubdirectory: true})); + } + + if (res.locals.apiVersion !== 'v0.1' && res.locals.apiVersion !== 'v2') { + post.access = !!post.html; + } + + // @TODO: See helpers/secure + helpers.secure(req, post); + + const renderer = helpers.renderEntry(req, res); + return renderer(post); + }) + .catch(helpers.handleError(next)); +}; diff --git a/core/frontend/services/routing/controllers/index.js b/core/frontend/services/routing/controllers/index.js index f97424e03f..8c9c4e6166 100644 --- a/core/frontend/services/routing/controllers/index.js +++ b/core/frontend/services/routing/controllers/index.js @@ -15,6 +15,10 @@ module.exports = { return require('./preview'); }, + get email() { + return require('./email-post'); + }, + get channel() { return require('./channel'); }, diff --git a/test/regression/site/email_routes.test.js b/test/regression/site/email_routes.test.js index ee0cc06ecb..7ce2449d07 100644 --- a/test/regression/site/email_routes.test.js +++ b/test/regression/site/email_routes.test.js @@ -10,16 +10,15 @@ const cheerio = require('cheerio'); const testUtils = require('../../utils'); const config = require('../../../core/shared/config'); const settingsCache = require('../../../core/shared/settings-cache'); +const bridge = require('../../../core/bridge'); describe('Frontend Routing: Email Routes', function () { let request; let emailPosts; - afterEach(function () { - sinon.restore(); - }); - before(async function () { + sinon.stub(bridge, 'getFrontendApiVersion') + .returns('v4'); const originalSettingsCacheGetFn = settingsCache.get; // NOTE: this wacky stubbing can be removed once emailOnlyPosts enters GA stage @@ -39,6 +38,12 @@ describe('Frontend Routing: Email Routes', function () { emailPosts = await testUtils.fixtures.insertPosts([{ title: 'I am visible through email route!', + status: 'sent', + posts_meta: { + email_only: true + } + }, { + title: 'I am NOT visible through email route!', status: 'draft', posts_meta: { email_only: true @@ -51,7 +56,7 @@ describe('Frontend Routing: Email Routes', function () { }); it('should display email_only post', async function () { - const res = await request.get(`/email/${emailPosts[0].get('uuid')}/`) + const res = await request.get(`/email/${emailPosts[0].get('slug')}/`) .expect('Content-Type', /html/) .expect(200); @@ -65,13 +70,13 @@ describe('Frontend Routing: Email Routes', function () { should.exist(res.headers.date); }); - it('404s when accessed by slug', function () { - return request.get(`/${emailPosts[0].get('slug')}/`) + it('404s for draft email only post', function () { + return request.get(`/email/${emailPosts[1].get('slug')}/`) .expect(404); }); - it('404s unknown uuids', function () { - return request.get('/email/aac6b4f6-e1f3-406c-9247-c94a0496d39f/') + it('404s unknown slug', function () { + return request.get('/email/random-slug/') .expect(404); }); });