0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

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
This commit is contained in:
Naz 2021-08-13 12:09:11 +04:00
parent 2cae064575
commit 74280cfbea
8 changed files with 155 additions and 16 deletions

View file

@ -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;

View file

@ -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);
}
}
/**

View file

@ -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);

View file

@ -36,6 +36,13 @@ module.exports.QUERY = {
preview: {
controller: 'preview',
resource: 'preview'
},
email: {
controller: 'emailPost',
resource: 'email_posts',
options: {
slug: '%s'
}
}
};

View file

@ -36,6 +36,13 @@ module.exports.QUERY = {
preview: {
controller: 'preview',
resource: 'preview'
},
email: {
controller: 'emailPost',
resource: 'email_posts',
options: {
slug: '%s'
}
}
};

View file

@ -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));
};

View file

@ -15,6 +15,10 @@ module.exports = {
return require('./preview');
},
get email() {
return require('./email-post');
},
get channel() {
return require('./channel');
},

View file

@ -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);
});
});