mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
🙅🏽 Admin server split (#8142)
refs #8140 ✨ Support new default-prod.hbs template for admin ✨ Redirect ghost admin urls without a # ✨ Update admin urls to include # 🎨 Move the admin templates 🔥 Remove redirect to setup middleware 🚨 Tests for new middleware
This commit is contained in:
parent
f4a68a2e52
commit
4a6f58d8d1
15 changed files with 102 additions and 175 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -65,7 +65,7 @@ config.*.json
|
||||||
|
|
||||||
# Built asset files
|
# Built asset files
|
||||||
/core/built
|
/core/built
|
||||||
/core/server/views/default.hbs
|
/core/server/admin/views/default*.hbs
|
||||||
/core/shared/ghost-url.min.js
|
/core/shared/ghost-url.min.js
|
||||||
|
|
||||||
# Coverage reports
|
# Coverage reports
|
||||||
|
|
|
@ -46,7 +46,7 @@ var overrides = require('./core/server/overrides'),
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
pkg: grunt.file.readJSON('package.json'),
|
||||||
|
|
||||||
clientFiles: [
|
clientFiles: [
|
||||||
'server/views/default.hbs',
|
'server/admin/views/default.hbs',
|
||||||
'built/assets/ghost.js',
|
'built/assets/ghost.js',
|
||||||
'built/assets/ghost.css',
|
'built/assets/ghost.css',
|
||||||
'built/assets/vendor.js',
|
'built/assets/vendor.js',
|
||||||
|
@ -465,7 +465,7 @@ var overrides = require('./core/server/overrides'),
|
||||||
return grunt.task.run(['lint']);
|
return grunt.task.run(['lint']);
|
||||||
}
|
}
|
||||||
|
|
||||||
grunt.task.run(['stubClientFiles', 'test-all']);
|
grunt.task.run(['test-all']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ### Test-All
|
// ### Test-All
|
||||||
|
@ -486,7 +486,7 @@ var overrides = require('./core/server/overrides'),
|
||||||
// ### test-setup *(utility)(
|
// ### test-setup *(utility)(
|
||||||
// `grunt test-setup` will run all the setup tasks required for running tests
|
// `grunt test-setup` will run all the setup tasks required for running tests
|
||||||
grunt.registerTask('test-setup', 'Setup ready to run tests',
|
grunt.registerTask('test-setup', 'Setup ready to run tests',
|
||||||
['knex-migrator', 'clean:test', 'setTestEnv']
|
['knex-migrator', 'clean:test', 'setTestEnv', 'stubClientFiles']
|
||||||
);
|
);
|
||||||
|
|
||||||
// ### Unit Tests *(sub task)*
|
// ### Unit Tests *(sub task)*
|
||||||
|
|
|
@ -4,7 +4,7 @@ var debug = require('debug')('ghost:admin'),
|
||||||
adminHbs = require('express-hbs').create(),
|
adminHbs = require('express-hbs').create(),
|
||||||
|
|
||||||
// Admin only middleware
|
// Admin only middleware
|
||||||
redirectToSetup = require('../middleware/redirect-to-setup'),
|
adminMiddleware = require('./middleware'),
|
||||||
|
|
||||||
// Global/shared middleware?
|
// Global/shared middleware?
|
||||||
cacheControl = require('../middleware/cache-control'),
|
cacheControl = require('../middleware/cache-control'),
|
||||||
|
@ -61,7 +61,7 @@ module.exports = function setupAdminApp() {
|
||||||
// Admin is currently set to not be cached at all
|
// Admin is currently set to not be cached at all
|
||||||
adminApp.use(cacheControl('private'));
|
adminApp.use(cacheControl('private'));
|
||||||
// Special redirects for the admin (these should have their own cache-control headers)
|
// Special redirects for the admin (these should have their own cache-control headers)
|
||||||
adminApp.use(redirectToSetup);
|
adminApp.use(adminMiddleware);
|
||||||
|
|
||||||
// Finally, routing
|
// Finally, routing
|
||||||
adminApp.get('*', require('./controller'));
|
adminApp.get('*', require('./controller'));
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
var debug = require('debug')('ghost:admin:controller'),
|
var debug = require('debug')('ghost:admin:controller'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
|
config = require('../config'),
|
||||||
api = require('../api'),
|
api = require('../api'),
|
||||||
updateCheck = require('../update-check'),
|
updateCheck = require('../update-check'),
|
||||||
logging = require('../logging'),
|
logging = require('../logging'),
|
||||||
|
@ -33,7 +34,9 @@ module.exports = function adminController(req, res) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).finally(function noMatterWhat() {
|
}).finally(function noMatterWhat() {
|
||||||
res.render('default');
|
var defaultTemplate = config.get('env') === 'production' ? 'default-prod' : 'default';
|
||||||
|
|
||||||
|
res.render(defaultTemplate);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
logging.error(err);
|
logging.error(err);
|
||||||
});
|
});
|
||||||
|
|
14
core/server/admin/middleware.js
Normal file
14
core/server/admin/middleware.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
var utils = require('../utils');
|
||||||
|
|
||||||
|
function redirectAdminUrls(req, res, next) {
|
||||||
|
var ghostPathMatch = req.originalUrl.match(/^\/ghost\/(.+)$/);
|
||||||
|
if (ghostPathMatch) {
|
||||||
|
return res.redirect(utils.url.urlJoin(utils.url.urlFor('admin'), '#', ghostPathMatch[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
redirectAdminUrls
|
||||||
|
];
|
0
core/server/admin/views/.gitkeep
Normal file
0
core/server/admin/views/.gitkeep
Normal file
|
@ -13,10 +13,10 @@ frontendRoutes = function frontendRoutes() {
|
||||||
|
|
||||||
// ### Admin routes
|
// ### Admin routes
|
||||||
router.get(/^\/(logout|signout)\/$/, function redirectToSignout(req, res) {
|
router.get(/^\/(logout|signout)\/$/, function redirectToSignout(req, res) {
|
||||||
utils.redirect301(res, utils.url.urlJoin(utils.url.urlFor('admin'), 'signout/'));
|
utils.redirect301(res, utils.url.urlJoin(utils.url.urlFor('admin'), '#/signout/'));
|
||||||
});
|
});
|
||||||
router.get(/^\/signup\/$/, function redirectToSignup(req, res) {
|
router.get(/^\/signup\/$/, function redirectToSignup(req, res) {
|
||||||
utils.redirect301(res, utils.url.urlJoin(utils.url.urlFor('admin'), 'signup/'));
|
utils.redirect301(res, utils.url.urlJoin(utils.url.urlFor('admin'), '#/signup/'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
|
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"corePath": "core/",
|
"corePath": "core/",
|
||||||
"clientAssets": "core/built/assets",
|
"clientAssets": "core/built/assets",
|
||||||
"helperTemplates": "core/server/helpers/tpl/",
|
"helperTemplates": "core/server/helpers/tpl/",
|
||||||
"adminViews": "core/server/views/",
|
"adminViews": "core/server/admin/views/",
|
||||||
"defaultViews": "core/server/views/",
|
"defaultViews": "core/server/views/",
|
||||||
"internalAppPath": "core/server/apps/",
|
"internalAppPath": "core/server/apps/",
|
||||||
"internalStoragePath": "core/server/storage/",
|
"internalStoragePath": "core/server/storage/",
|
||||||
|
|
|
@ -24,7 +24,7 @@ channelConfig = function channelConfig() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
slugTemplate: true,
|
slugTemplate: true,
|
||||||
editRedirect: utils.url.urlJoin(utils.url.urlFor('admin'), '/settings/tags/:slug/')
|
editRedirect: utils.url.urlJoin(utils.url.urlFor('admin'), '#/settings/tags/:slug/')
|
||||||
},
|
},
|
||||||
author: {
|
author: {
|
||||||
name: 'author',
|
name: 'author',
|
||||||
|
@ -40,7 +40,7 @@ channelConfig = function channelConfig() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
slugTemplate: true,
|
slugTemplate: true,
|
||||||
editRedirect: utils.url.urlJoin(utils.url.urlFor('admin'), '/team/:slug/')
|
editRedirect: utils.url.urlJoin(utils.url.urlFor('admin'), '#/team/:slug/')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
var api = require('../api'),
|
|
||||||
utils = require('../utils');
|
|
||||||
|
|
||||||
// Redirect to setup if no user exists
|
|
||||||
function redirectToSetup(req, res, next) {
|
|
||||||
var isSetupRequest = req.path.match(/\/setup\//),
|
|
||||||
isOauthAuthorization = req.path.match(/\/$/) && req.query && (req.query.code || req.query.error);
|
|
||||||
|
|
||||||
api.authentication.isSetup().then(function then(exists) {
|
|
||||||
if (!exists.setup[0].status && !isSetupRequest && !isOauthAuthorization) {
|
|
||||||
return res.redirect(utils.url.urlJoin(utils.url.urlFor('admin') + 'setup/'));
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}).catch(function handleError(err) {
|
|
||||||
return next(new Error(err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = redirectToSetup;
|
|
|
@ -82,25 +82,25 @@ describe('Admin Routing', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Legacy Redirects', function () {
|
describe('Legacy Redirects', function () {
|
||||||
it('should redirect /logout/ to /ghost/signout/', function (done) {
|
it('should redirect /logout/ to /ghost/#/signout/', function (done) {
|
||||||
request.get('/logout/')
|
request.get('/logout/')
|
||||||
.expect('Location', '/ghost/signout/')
|
.expect('Location', '/ghost/#/signout/')
|
||||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||||
.expect(301)
|
.expect(301)
|
||||||
.end(doEndNoAuth(done));
|
.end(doEndNoAuth(done));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect /signout/ to /ghost/signout/', function (done) {
|
it('should redirect /signout/ to /ghost/#/signout/', function (done) {
|
||||||
request.get('/signout/')
|
request.get('/signout/')
|
||||||
.expect('Location', '/ghost/signout/')
|
.expect('Location', '/ghost/#/signout/')
|
||||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||||
.expect(301)
|
.expect(301)
|
||||||
.end(doEndNoAuth(done));
|
.end(doEndNoAuth(done));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect /signup/ to /ghost/signup/', function (done) {
|
it('should redirect /signup/ to /ghost/#/signup/', function (done) {
|
||||||
request.get('/signup/')
|
request.get('/signup/')
|
||||||
.expect('Location', '/ghost/signup/')
|
.expect('Location', '/ghost/#/signup/')
|
||||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||||
.expect(301)
|
.expect(301)
|
||||||
.end(doEndNoAuth(done));
|
.end(doEndNoAuth(done));
|
||||||
|
@ -130,32 +130,6 @@ describe('Admin Routing', function () {
|
||||||
.end(doEndNoAuth(done));
|
.end(doEndNoAuth(done));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Ghost Admin Setup', function () {
|
|
||||||
it('should redirect from /ghost/ to /ghost/setup/ when no user/not installed yet', function (done) {
|
|
||||||
request.get('/ghost/')
|
|
||||||
.expect('Location', /ghost\/setup/)
|
|
||||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
||||||
.expect(302)
|
|
||||||
.end(doEnd(done));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect from /ghost/signin/ to /ghost/setup/ when no user', function (done) {
|
|
||||||
request.get('/ghost/signin/')
|
|
||||||
.expect('Location', /ghost\/setup/)
|
|
||||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
||||||
.expect(302)
|
|
||||||
.end(doEnd(done));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should respond with html for /ghost/setup/', function (done) {
|
|
||||||
request.get('/ghost/setup/')
|
|
||||||
.expect('Content-Type', /html/)
|
|
||||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
|
||||||
.expect(200)
|
|
||||||
.end(doEnd(done));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('FORK', function () {
|
describe('FORK', function () {
|
||||||
|
@ -194,7 +168,7 @@ describe('Admin Routing', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow admin access over HTTPS', function (done) {
|
it('should allow admin access over HTTPS', function (done) {
|
||||||
request.get('/ghost/setup/')
|
request.get('/ghost/')
|
||||||
.set('X-Forwarded-Proto', 'https')
|
.set('X-Forwarded-Proto', 'https')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end(doEnd(done));
|
.end(doEnd(done));
|
||||||
|
|
|
@ -380,7 +380,7 @@ describe('Channel Routes', function () {
|
||||||
|
|
||||||
it('should redirect to tag settings', function (done) {
|
it('should redirect to tag settings', function (done) {
|
||||||
request.get('/tag/getting-started/edit/')
|
request.get('/tag/getting-started/edit/')
|
||||||
.expect('Location', '/ghost/settings/tags/getting-started/')
|
.expect('Location', '/ghost/#/settings/tags/getting-started/')
|
||||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||||
.expect(302)
|
.expect(302)
|
||||||
.end(doEnd(done));
|
.end(doEnd(done));
|
||||||
|
@ -549,7 +549,7 @@ describe('Channel Routes', function () {
|
||||||
|
|
||||||
it('should redirect to editor', function (done) {
|
it('should redirect to editor', function (done) {
|
||||||
request.get('/author/ghost-owner/edit/')
|
request.get('/author/ghost-owner/edit/')
|
||||||
.expect('Location', '/ghost/team/ghost-owner/')
|
.expect('Location', '/ghost/#/team/ghost-owner/')
|
||||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||||
.expect(302)
|
.expect(302)
|
||||||
.end(doEnd(done));
|
.end(doEnd(done));
|
||||||
|
|
63
core/test/unit/admin_spec.js
Normal file
63
core/test/unit/admin_spec.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
var should = require('should'), // jshint ignore:line
|
||||||
|
sinon = require('sinon'),
|
||||||
|
|
||||||
|
// Thing we are testing
|
||||||
|
redirectAdminUrls = require('../../server/admin/middleware')[0],
|
||||||
|
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
describe('Admin App', function () {
|
||||||
|
afterEach(function () {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('middleware', function () {
|
||||||
|
describe('redirectAdminUrls', function () {
|
||||||
|
var req, res, next;
|
||||||
|
// Input: req.originalUrl
|
||||||
|
// Output: either next or res.redirect are called
|
||||||
|
beforeEach(function () {
|
||||||
|
req = {};
|
||||||
|
res = {};
|
||||||
|
next = sandbox.stub();
|
||||||
|
res.redirect = sandbox.stub();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect a url which starts with ghost', function () {
|
||||||
|
req.originalUrl = '/ghost/x';
|
||||||
|
|
||||||
|
redirectAdminUrls(req, res, next);
|
||||||
|
|
||||||
|
next.called.should.be.false();
|
||||||
|
res.redirect.called.should.be.true();
|
||||||
|
res.redirect.calledWith('/ghost/#/x').should.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect /ghost/ on its owh', function () {
|
||||||
|
req.originalUrl = '/ghost/';
|
||||||
|
|
||||||
|
redirectAdminUrls(req, res, next);
|
||||||
|
|
||||||
|
next.called.should.be.true();
|
||||||
|
res.redirect.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect url that has no slash', function () {
|
||||||
|
req.originalUrl = 'ghost/x';
|
||||||
|
|
||||||
|
redirectAdminUrls(req, res, next);
|
||||||
|
|
||||||
|
next.called.should.be.true();
|
||||||
|
res.redirect.called.should.be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect url that starts with something other than /ghost/', function () {
|
||||||
|
req.originalUrl = 'x/ghost/x';
|
||||||
|
|
||||||
|
redirectAdminUrls(req, res, next);
|
||||||
|
|
||||||
|
next.called.should.be.true();
|
||||||
|
res.redirect.called.should.be.false();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -381,7 +381,7 @@ describe('Channels', function () {
|
||||||
describe('Edit', function () {
|
describe('Edit', function () {
|
||||||
it('should redirect /edit/ to ghost admin', function (done) {
|
it('should redirect /edit/ to ghost admin', function (done) {
|
||||||
testChannelRedirect({url: '/tag/my-tag/edit/'}, function (path) {
|
testChannelRedirect({url: '/tag/my-tag/edit/'}, function (path) {
|
||||||
path.should.eql('/ghost/settings/tags/my-tag/');
|
path.should.eql('/ghost/#/settings/tags/my-tag/');
|
||||||
postAPIStub.called.should.be.false();
|
postAPIStub.called.should.be.false();
|
||||||
}, done);
|
}, done);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
var sinon = require('sinon'),
|
|
||||||
should = require('should'),
|
|
||||||
Promise = require('bluebird'),
|
|
||||||
api = require('../../../server/api'),
|
|
||||||
redirectToSetup = require('../../../server/middleware/redirect-to-setup');
|
|
||||||
|
|
||||||
should.equal(true, true);
|
|
||||||
|
|
||||||
describe('redirectToSetup', function () {
|
|
||||||
var res, req, next, sandbox;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
sandbox = sinon.sandbox.create();
|
|
||||||
|
|
||||||
res = sinon.spy();
|
|
||||||
req = sinon.spy();
|
|
||||||
next = sinon.spy();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to setup if not on setup', function (done) {
|
|
||||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
|
||||||
return Promise.resolve({setup: [{status: false}]});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.path = '/';
|
|
||||||
res.redirect = sinon.spy(function () {
|
|
||||||
next.called.should.be.false();
|
|
||||||
res.redirect.called.should.be.true();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectToSetup(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect if setup is done', function (done) {
|
|
||||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
|
||||||
return Promise.resolve({setup: [{status: true}]});
|
|
||||||
});
|
|
||||||
|
|
||||||
res = {redirect: sinon.spy()};
|
|
||||||
req.path = '/';
|
|
||||||
|
|
||||||
next = sinon.spy(function () {
|
|
||||||
next.called.should.be.true();
|
|
||||||
res.redirect.called.should.be.false();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectToSetup(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect if already on setup', function (done) {
|
|
||||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
|
||||||
return Promise.resolve({setup: [{status: false}]});
|
|
||||||
});
|
|
||||||
|
|
||||||
res = {redirect: sinon.spy()};
|
|
||||||
req.path = '/ghost/setup/';
|
|
||||||
|
|
||||||
next = sinon.spy(function () {
|
|
||||||
next.called.should.be.true();
|
|
||||||
res.redirect.called.should.be.false();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectToSetup(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect successful oauth authorization requests', function (done) {
|
|
||||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
|
||||||
return Promise.resolve({setup: [{status: false}]});
|
|
||||||
});
|
|
||||||
|
|
||||||
res = {redirect: sinon.spy()};
|
|
||||||
req.path = '/';
|
|
||||||
req.query = {code: 'authCode'};
|
|
||||||
|
|
||||||
next = sinon.spy(function () {
|
|
||||||
next.called.should.be.true();
|
|
||||||
res.redirect.called.should.be.false();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectToSetup(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not redirect failed oauth authorization requests', function (done) {
|
|
||||||
sandbox.stub(api.authentication, 'isSetup', function () {
|
|
||||||
return Promise.resolve({setup: [{status: false}]});
|
|
||||||
});
|
|
||||||
|
|
||||||
res = {redirect: sinon.spy()};
|
|
||||||
req.path = '/';
|
|
||||||
req.query = {error: 'access_denied', state: 'randomstring'};
|
|
||||||
|
|
||||||
next = sinon.spy(function () {
|
|
||||||
next.called.should.be.true();
|
|
||||||
res.redirect.called.should.be.false();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
redirectToSetup(req, res, next);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Reference in a new issue