Skip to content

Commit

Permalink
πŸŽ‰ 🎨 ✨ Remove middleware/index.js (#7548)
Browse files Browse the repository at this point in the history
closes #4172, closes #6948, refs #7491, refs #7488, refs #7542, refs #7484

* 🎨 Co-locate all admin-related code in /admin
- move all the admin related code from controllers, routes and helpers into a single location
- add error handling middleware explicitly to adminApp
- re-order blogApp middleware to ensure the shared middleware is mounted after the adminApp
- TODO: rethink the structure of /admin, this should probably be an internal app

* πŸ’„ Group global middleware together

- There are only a few pieces of middleware which are "global"
- These are needed for the admin, blog and api
- Everything else is only needed in one or two places

* ✨ Introduce a separate blogApp

- create a brand-new blogApp
- mount all blog/theme only middleware etc onto blogApp
- mount error handling on blogApp only

* 🎨 Separate error handling for HTML & API JSON

- split JSON and HTML error handling into separate functions
- re-introduce a way to not output the stack for certain errors
- add more tests around errors & an assertion framework for checking JSON Errors
- TODO: better 404 handling for static assets

Rationale:

The API is very different to the blog/admin panel:
 - It is intended to only ever serve JSON, never HTML responses
 - It is intended to always serve JSON

Meanwhile the blog and admin panel have no need for JSON errors,
when an error happens on those pages, we should serve HTML pages
which are nicely formatted with the error & using the correct template

* πŸ› Fix checkSSL to work for subapps

- in order to make this work on a sub app we need to use the pattern `req.originalUrl || req.url`

* πŸ”₯ Get rid of decide-is-admin (part 1/2)

- delete decide-is-admin & tests
- add two small functions to apiApp and adminApp to set res.isAdmin
- mount checkSSL on all the apps
- TODO: deduplicate the calls to checkSSL by making blogApp a subApp :D
- PART 2/2: finish cleaning this up by removing it from where it's not needed and giving it a more specific name

Rationale:

Now that we have both an adminApp and an apiApp,
we can temporarily replace this weird path-matching middleware
with middleware that sets res.isAdmin for api & admin

* 🎨 Wire up prettyURLs on all Apps

- prettyURLs is needed for all requests
- it cannot be global because it has to live after asset middleware, and before routing
- this does not result in duplicate redirects, but does result in duplicate checks
- TODO: resolve extra middleware in stack by making blogApp a sub app

* ⏱ Add debug to API setup

* 🎨 Rename blogApp -> parentApp in middleware

* 🎨 Co-locate all blog-related code in /blog

- Move all of the blogApp code from middleware/index.js to blog/app.js
- Move routes/frontend.js to blog/routes.js
- Remove the routes/index.js and routes folder, this is empty now!
- @todo is blog the best name for this? πŸ€”
- @todo sort out the big hunk of asset-related mess
- @todo also separate out the concept of theme from blog

* πŸŽ‰ Replace middleware index with server/app.js

- The final piece of the puzzle! πŸŽ‰ 🎈 πŸŽ‚
- We no longer have our horrendous middleware/index.js
- Instead, we have a set of app.js files, which all use a familiar pattern

* πŸ’„ Error handling fixups
  • Loading branch information
ErisDS authored and kirrg001 committed Oct 13, 2016
1 parent 4abb959 commit 4411f82
Show file tree
Hide file tree
Showing 26 changed files with 600 additions and 484 deletions.
70 changes: 70 additions & 0 deletions core/server/admin/app.js
@@ -0,0 +1,70 @@
var debug = require('debug')('ghost:admin'),
config = require('../config'),
express = require('express'),
adminHbs = require('express-hbs').create(),

// Admin only middleware
redirectToSetup = require('../middleware/redirect-to-setup'),

// Global/shared middleware?
cacheControl = require('../middleware/cache-control'),
checkSSL = require('../middleware/check-ssl'),
errorHandler = require('../middleware//error-handler'),
maintenance = require('../middleware/maintenance'),
prettyURLs = require('../middleware//pretty-urls'),
serveStatic = require('express').static,
utils = require('../utils');

module.exports = function setupAdminApp() {
debug('Admin setup start');
var adminApp = express();

// First determine whether we're serving admin or theme content
// @TODO finish refactoring this away.
adminApp.use(function setIsAdmin(req, res, next) {
res.isAdmin = true;
next();
});

// @TODO replace all this with serving ember's index.html
// Create a hbs instance for admin and init view engine
adminApp.set('view engine', 'hbs');
adminApp.set('views', config.get('paths').adminViews);
adminApp.engine('hbs', adminHbs.express3({}));
// Register our `asset` helper
adminHbs.registerHelper('asset', require('../helpers/asset'));

// Admin assets
// @TODO ensure this gets a local 404 error handler
adminApp.use('/assets', serveStatic(
config.get('paths').clientAssets,
{maxAge: utils.ONE_YEAR_MS, fallthrough: false}
));

// Render error page in case of maintenance
adminApp.use(maintenance);

// Force SSL if required
// must happen AFTER asset loading and BEFORE routing
adminApp.use(checkSSL);

// Add in all trailing slashes & remove uppercase
// must happen AFTER asset loading and BEFORE routing
adminApp.use(prettyURLs);

// Cache headers go last before serving the request
// Admin is currently set to not be cached at all
adminApp.use(cacheControl('private'));
// Special redirects for the admin (these should have their own cache-control headers)
adminApp.use(redirectToSetup);

// Finally, routing
adminApp.get('*', require('./controller'));

adminApp.use(errorHandler.pageNotFound);
adminApp.use(errorHandler.handleHTMLResponse);

debug('Admin setup end');

return adminApp;
};
69 changes: 69 additions & 0 deletions core/server/admin/controller.js
@@ -0,0 +1,69 @@
var debug = require('debug')('ghost:admin:controller'),
_ = require('lodash'),
Promise = require('bluebird'),
api = require('../api'),
config = require('../config'),
logging = require('../logging'),
updateCheck = require('../update-check'),
i18n = require('../i18n');

// Route: index
// Path: /ghost/
// Method: GET
module.exports = function adminController(req, res) {
/*jslint unparam:true*/
debug('index called');

function renderIndex() {
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]; }),
ghostAuth: api.clients.read({slug: 'ghost-auth'})
.then(function (res) { return res.clients[0]; })
.catch(function () {
return;
})
};

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'};

if (result.ghostAuth && config.get('auth:type') === 'ghost') {
configuration.ghostAuthId = {value: result.ghostAuth.uuid, type: 'string'};
}

debug('rendering default template');
res.render('default', {
configuration: configuration
});
});
}

updateCheck().then(function then() {
return updateCheck.showUpdateNotification();
}).then(function then(updateVersion) {
if (!updateVersion) {
return;
}

var notification = {
status: 'alert',
type: 'info',
location: 'upgrade.new-version-available',
dismissible: false,
message: i18n.t('notices.controllers.newVersionAvailable',
{version: updateVersion, link: '<a href="http://support.ghost.org/how-to-upgrade/" target="_blank">Click here</a>'})};

return api.notifications.browse({context: {internal: true}}).then(function then(results) {
if (!_.some(results.notifications, {message: notification.message})) {
return api.notifications.add({notifications: [notification]}, {context: {internal: true}});
}
});
}).finally(function noMatterWhat() {
renderIndex();
}).catch(logging.logError);
};
1 change: 1 addition & 0 deletions core/server/admin/index.js
@@ -0,0 +1 @@
module.exports = require('./app');
27 changes: 23 additions & 4 deletions core/server/api/app.js
@@ -1,5 +1,6 @@
// # API routes
var express = require('express'),
var debug = require('debug')('ghost:api'),
express = require('express'),
tmpdir = require('os').tmpdir,

// This essentially provides the controllers for the routes
Expand All @@ -20,6 +21,8 @@ var express = require('express'),
// Shared
bodyParser = require('body-parser'), // global, shared
cacheControl = require('../middleware/cache-control'), // global, shared
checkSSL = require('../middleware/check-ssl'),
prettyURLs = require('../middleware/pretty-urls'),
maintenance = require('../middleware/maintenance'), // global, shared
errorHandler = require('../middleware/error-handler'), // global, shared

Expand Down Expand Up @@ -209,8 +212,16 @@ function apiRoutes() {
}

module.exports = function setupApiApp() {
debug('API setup start');
var apiApp = express();

// @TODO finish refactoring this away.
apiApp.use(function setIsAdmin(req, res, next) {
// Api === isAdmin for the purposes of the forceAdminSSL config option.
res.isAdmin = true;
next();
});

// API middleware

// Body parsing
Expand All @@ -220,7 +231,13 @@ module.exports = function setupApiApp() {
// send 503 json response in case of maintenance
apiApp.use(maintenance);

// @TODO check SSL and pretty URLS is needed here too, once we've finished refactoring
// Force SSL if required
// must happen AFTER asset loading and BEFORE routing
apiApp.use(checkSSL);

// 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
Expand All @@ -233,8 +250,10 @@ module.exports = function setupApiApp() {
apiApp.use(apiRoutes());

// API error handling
// @TODO: split the API error handling into its own thing?
apiApp.use(errorHandler);
apiApp.use(errorHandler.resourceNotFound);
apiApp.use(errorHandler.handleJSONResponse);

debug('API setup end');

return apiApp;
};
84 changes: 84 additions & 0 deletions core/server/app.js
@@ -0,0 +1,84 @@
var debug = require('debug')('ghost:app'),
express = require('express'),

// app requires
config = require('./config'),
logging = require('./logging'),

// middleware
compress = require('compression'),
netjet = require('netjet'),

// local middleware
ghostLocals = require('./middleware/ghost-locals');

module.exports = function setupParentApp() {
debug('ParentApp setup start');
var parentApp = express();

// ## Global settings

// Make sure 'req.secure' is valid for proxied requests
// (X-Forwarded-Proto header will be checked, if present)
parentApp.enable('trust proxy');

/**
* request logging
*/
parentApp.use(function expressLogging(req, res, next) {
res.once('finish', function () {
logging.request({req: req, res: res, err: req.err});
});

next();
});

if (debug.enabled) {
// debug keeps a timer, so this is super useful
parentApp.use((function () {
var reqDebug = require('debug')('ghost:req');
return function debugLog(req, res, next) {
reqDebug('Request', req.originalUrl);
next();
};
})());
}

// enabled gzip compression by default
if (config.get('server').compress !== false) {
parentApp.use(compress());
}

// Preload link headers
if (config.get('preloadHeaders')) {
parentApp.use(netjet({
cache: {
max: config.get('preloadHeaders')
}
}));
}

// This sets global res.locals which are needed everywhere
parentApp.use(ghostLocals);

// @TODO where should this live?!
// Load helpers
require('./helpers').loadCoreHelpers();
debug('Helpers done');

// Mount the apps on the parentApp
// API
// @TODO: finish refactoring the API app
// @TODO: decide what to do with these paths - config defaults? config overrides?
parentApp.use('/ghost/api/v0.1/', require('./api/app')());

// ADMIN
parentApp.use('/ghost', require('./admin')());

// BLOG
parentApp.use(require('./blog')());

debug('ParentApp setup end');

return parentApp;
};
99 changes: 99 additions & 0 deletions core/server/blog/app.js
@@ -0,0 +1,99 @@
var debug = require('debug')('ghost:blog'),
path = require('path'),

// App requires
config = require('../config'),
storage = require('../storage'),
utils = require('../utils'),

// This should probably be an internal app
sitemapHandler = require('../data/xml/sitemap/handler'),

// routes
routes = require('./routes'),

// local middleware
cacheControl = require('../middleware/cache-control'),
checkSSL = require('../middleware/check-ssl'),
errorHandler = require('../middleware/error-handler'),
maintenance = require('../middleware/maintenance'),
prettyURLs = require('../middleware/pretty-urls'),
serveSharedFile = require('../middleware/serve-shared-file'),
staticTheme = require('../middleware/static-theme'),
themeHandler = require('../middleware/theme-handler');

module.exports = function setupBlogApp() {
debug('Blog setup start');

var blogApp = require('express')();

// ## App - specific code
// set the view engine
blogApp.set('view engine', 'hbs');

// Theme middleware
// rightly or wrongly currently comes before theme static assets
// @TODO revisit where and when these are needed
blogApp.use(themeHandler.updateActiveTheme);
blogApp.use(themeHandler.configHbsForContext);
debug('Themes done');

// Static content/assets
// @TODO make sure all of these have a local 404 error handler
// Favicon
blogApp.use(serveSharedFile('favicon.ico', 'image/x-icon', utils.ONE_DAY_S));
// Ghost-Url
blogApp.use(serveSharedFile('shared/ghost-url.js', 'application/javascript', utils.ONE_HOUR_S));
blogApp.use(serveSharedFile('shared/ghost-url.min.js', 'application/javascript', utils.ONE_HOUR_S));
// Serve sitemap.xsl file
blogApp.use(serveSharedFile('sitemap.xsl', 'text/xsl', utils.ONE_DAY_S));
// Serve robots.txt if not found in theme
blogApp.use(serveSharedFile('robots.txt', 'text/plain', utils.ONE_HOUR_S));
// Serve blog images using the storage adapter
blogApp.use('/content/images', storage.getStorage().serve());

// Theme static assets/files
blogApp.use(staticTheme());
debug('Static content done');

// setup middleware for internal apps
// @TODO: refactor this to be a proper app middleware hook for internal & external apps
config.get('internalApps').forEach(function (appName) {
var app = require(path.join(config.get('paths').internalAppPath, appName));
if (app.hasOwnProperty('setupMiddleware')) {
app.setupMiddleware(blogApp);
}
});

// site map - this should probably be refactored to be an internal app
sitemapHandler(blogApp);
debug('Internal apps done');

// send 503 error page in case of maintenance
blogApp.use(maintenance);

// Force SSL if required
// must happen AFTER asset loading and BEFORE routing
blogApp.use(checkSSL);

// Add in all trailing slashes & remove uppercase
// must happen AFTER asset loading and BEFORE routing
blogApp.use(prettyURLs);

// ### Caching
// Blog frontend is cacheable
blogApp.use(cacheControl('public'));

debug('General middleware done');

// Set up Frontend routes (including private blogging routes)
blogApp.use(routes());

// ### Error handlers
blogApp.use(errorHandler.pageNotFound);
blogApp.use(errorHandler.handleHTMLResponse);

debug('Blog setup end');

return blogApp;
};
1 change: 1 addition & 0 deletions core/server/blog/index.js
@@ -0,0 +1 @@
module.exports = require('./app');
File renamed without changes.

0 comments on commit 4411f82

Please sign in to comment.