mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
added password protection
closes #4993 - brings password protection to the frontend of blogs - adds testing for password protection - upgrades bcrypt-js to 2.1.0
This commit is contained in:
parent
f4a9f0c82d
commit
2865662ee5
24 changed files with 697 additions and 9 deletions
|
@ -24,6 +24,10 @@ var FeatureController = Ember.Controller.extend(Ember.PromiseProxyMixin, {
|
|||
}
|
||||
|
||||
return value;
|
||||
}),
|
||||
|
||||
passProtectUI: Ember.computed('config.passProtectUI', 'labs.passProtectUI', function () {
|
||||
return this.get('config.passProtectUI') || this.get('labs.passProtectUI');
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Ember from 'ember';
|
||||
var SettingsController = Ember.Controller.extend({
|
||||
|
||||
needs: ['feature'],
|
||||
|
||||
showGeneral: Ember.computed('session.user.name', function () {
|
||||
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') ? false : true;
|
||||
}),
|
||||
|
@ -21,6 +23,9 @@ var SettingsController = Ember.Controller.extend({
|
|||
}),
|
||||
showAbout: Ember.computed('session.user.name', function () {
|
||||
return this.get('session.user.isAuthor') ? false : true;
|
||||
}),
|
||||
showPassProtection: Ember.computed('session.user.name', 'controllers.feature.passProtectUI', function () {
|
||||
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') || !this.get('controllers.feature.passProtectUI') ? false : true;
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -23,6 +23,16 @@ var LabsController = Ember.Controller.extend(Ember.Evented, {
|
|||
});
|
||||
},
|
||||
|
||||
usePassProtectUI: Ember.computed('controllers.feature.passProtectUI', function (key, value) {
|
||||
// setter
|
||||
if (arguments.length > 1) {
|
||||
this.saveLabs('passProtectUI', value);
|
||||
}
|
||||
|
||||
// getter
|
||||
return this.get('controllers.feature.passProtectUI');
|
||||
}),
|
||||
|
||||
actions: {
|
||||
onUpload: function (file) {
|
||||
var self = this,
|
||||
|
|
27
core/client/app/controllers/settings/pass-protect.js
Normal file
27
core/client/app/controllers/settings/pass-protect.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
var SettingsPassProtectController = Ember.Controller.extend({
|
||||
|
||||
actions: {
|
||||
save: function () {
|
||||
var self = this;
|
||||
if (this.get('model.isPrivate') && this.get('model.password') === '') {
|
||||
self.notifications.closePassive();
|
||||
self.notifications.showError('Password must have a value.');
|
||||
return;
|
||||
}
|
||||
|
||||
return this.get('model').save().then(function (model) {
|
||||
self.notifications.closePassive();
|
||||
self.notifications.showSuccess('Settings successfully saved.');
|
||||
|
||||
return model;
|
||||
}).catch(function (errors) {
|
||||
self.notifications.closePassive();
|
||||
self.notifications.showErrors(errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default SettingsPassProtectController;
|
|
@ -19,7 +19,9 @@ var Setting = DS.Model.extend(NProgressSaveMixin, ValidationEngine, {
|
|||
ghost_head: DS.attr('string'),
|
||||
ghost_foot: DS.attr('string'),
|
||||
labs: DS.attr('string'),
|
||||
navigation: DS.attr('string')
|
||||
navigation: DS.attr('string'),
|
||||
isPrivate: DS.attr('boolean'),
|
||||
password: DS.attr('string')
|
||||
});
|
||||
|
||||
export default Setting;
|
||||
|
|
|
@ -43,6 +43,7 @@ Router.map(function () {
|
|||
this.route('labs');
|
||||
this.route('code-injection');
|
||||
this.route('navigation');
|
||||
this.route('pass-protect');
|
||||
});
|
||||
|
||||
// Redirect debug to settings labs
|
||||
|
|
45
core/client/app/routes/settings/pass-protect.js
Normal file
45
core/client/app/routes/settings/pass-protect.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import AuthenticatedRoute from 'ghost/routes/authenticated';
|
||||
import loadingIndicator from 'ghost/mixins/loading-indicator';
|
||||
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
|
||||
import styleBody from 'ghost/mixins/style-body';
|
||||
|
||||
var SettingsPassProtectRoute = AuthenticatedRoute.extend(styleBody, loadingIndicator, CurrentUserSettings, {
|
||||
|
||||
classNames: ['settings-view-pass'],
|
||||
|
||||
beforeModel: function () {
|
||||
var feature = this.controllerFor('feature'),
|
||||
self = this;
|
||||
|
||||
if (!feature) {
|
||||
this.generateController('feature');
|
||||
feature = this.controllerFor('feature');
|
||||
}
|
||||
|
||||
return this.get('session.user')
|
||||
.then(this.transitionAuthor())
|
||||
.then(this.transitionEditor())
|
||||
.then(function () {
|
||||
return feature.then(function () {
|
||||
if (!feature.get('passProtectUI')) {
|
||||
return self.transitionTo('settings.general');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
model: function () {
|
||||
return this.store.find('setting', {type: 'blog,theme'}).then(function (records) {
|
||||
return records.get('firstObject');
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
save: function () {
|
||||
this.get('controller').send('save');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default SettingsPassProtectRoute;
|
|
@ -27,6 +27,10 @@
|
|||
{{gh-activating-list-item route="settings.code-injection" title="Code Injection" classNames="settings-nav-code icon-code"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showPassProtection}}
|
||||
{{gh-activating-list-item route="settings.pass-protect" title="Password Protection" classNames="settings-nav-pass icon-lock"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showLabs}}
|
||||
{{gh-activating-list-item route="settings.labs" title="Labs" classNames="settings-nav-labs icon-atom"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -44,4 +44,21 @@
|
|||
</fieldset>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<form>
|
||||
<fieldset>
|
||||
<div class="form-group for-checkbox">
|
||||
<label for="labs-passProtectUI">Password Protection</label>
|
||||
<label class="checkbox" for="labs-passProtectUI">
|
||||
{{input id="labs-passProtectUI" name="labs[passProtectUI]" type="checkbox"
|
||||
checked=usePassProtectUI}}
|
||||
<span class="input-toggle-component"></span>
|
||||
<p>Enable the password protection interface</p>
|
||||
</label>
|
||||
<p>A settings screen which enables you to add password protection to the front of your blog (work in progress)</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
|
29
core/client/app/templates/settings/pass-protect.hbs
Normal file
29
core/client/app/templates/settings/pass-protect.hbs
Normal file
|
@ -0,0 +1,29 @@
|
|||
<header class="settings-view-header">
|
||||
{{#link-to "settings" class="btn btn-default btn-back"}}Back{{/link-to}}
|
||||
<h2 class="page-title">Password protect your blog</h2>
|
||||
<section class="page-actions">
|
||||
<button type="button" class="btn btn-blue" {{action "save"}}>Save</button>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section class="content settings-pass">
|
||||
<form id="settings-pass" novalidate="novalidate">
|
||||
<fieldset>
|
||||
<div class="form-group for-checkbox">
|
||||
<label for="blog-isPrivate">Make this blog private</label>
|
||||
<label class="checkbox" for="blog-isPrivate">
|
||||
{{input id="blog-isPrivate" name="labs[passProtectUI]" type="checkbox"
|
||||
checked=model.isPrivate}}
|
||||
<span class="input-toggle-component"></span>
|
||||
<p>Enable password protection</p>
|
||||
</label>
|
||||
</div>
|
||||
{{#if model.isPrivate}}
|
||||
<div class="form-group">
|
||||
{{input name="private[password]" type="text" value=model.password}}
|
||||
<p>This password will be needed to access your blog. All search engine optimization and social features are now disabled.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
5
core/client/app/views/settings/pass-protect.js
Normal file
5
core/client/app/views/settings/pass-protect.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import BaseView from 'ghost/views/settings/content-base';
|
||||
|
||||
var SettingsGeneralView = BaseView.extend();
|
||||
|
||||
export default SettingsGeneralView;
|
|
@ -10,6 +10,7 @@ var _ = require('lodash'),
|
|||
function getValidKeys() {
|
||||
var validKeys = {
|
||||
fileStorage: config.fileStorage === false ? false : true,
|
||||
passProtectUI: config.passProtectUI === true ? true : false,
|
||||
apps: config.apps === true ? true : false,
|
||||
version: config.ghostVersion,
|
||||
environment: process.env.NODE_ENV,
|
||||
|
|
|
@ -94,7 +94,7 @@ function urlPathForPost(post, permalinks) {
|
|||
// Usage:
|
||||
// urlFor('home', true) -> http://my-ghost-blog.com/
|
||||
// E.g. /blog/ subdir
|
||||
// urlFor({relativeUrl: '/my-static-page/') -> /blog/my-static-page/
|
||||
// urlFor({relativeUrl: '/my-static-page/'}) -> /blog/my-static-page/
|
||||
// E.g. if post object represents welcome post, and slugs are set to standard
|
||||
// urlFor('post', {...}) -> /welcome-to-ghost/
|
||||
// E.g. if post object represents welcome post, and slugs are set to date
|
||||
|
|
|
@ -14,6 +14,7 @@ var moment = require('moment'),
|
|||
template = require('../helpers/template'),
|
||||
errors = require('../errors'),
|
||||
routeMatch = require('path-match')(),
|
||||
path = require('path'),
|
||||
|
||||
frontendControllers,
|
||||
staticPostPermalink;
|
||||
|
@ -451,7 +452,23 @@ frontendControllers = {
|
|||
return handleError(next)(err);
|
||||
});
|
||||
},
|
||||
rss: rss
|
||||
rss: rss,
|
||||
private: function (req, res) {
|
||||
var defaultPage = path.resolve(config.paths.adminViews, 'password.hbs');
|
||||
return getActiveThemePaths().then(function (paths) {
|
||||
var data = {
|
||||
forward: req.query.r
|
||||
};
|
||||
if (res.error) {
|
||||
data.error = res.error;
|
||||
}
|
||||
if (paths.hasOwnProperty('password.hbs')) {
|
||||
return res.render('password', data);
|
||||
} else {
|
||||
return res.render(defaultPage, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = frontendControllers;
|
||||
|
|
|
@ -73,6 +73,12 @@
|
|||
},
|
||||
"navigation": {
|
||||
"defaultValue": "[{\"label\":\"Home\", \"url\":\"/\"}]"
|
||||
},
|
||||
"isPrivate": {
|
||||
"defaultValue": "false"
|
||||
},
|
||||
"password": {
|
||||
"defaultValue": ""
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
|
|
|
@ -249,7 +249,6 @@ setupMiddleware = function (blogAppInstance, adminApp) {
|
|||
|
||||
// Favicon
|
||||
blogApp.use(serveSharedFile('favicon.ico', 'image/x-icon', utils.ONE_DAY_S));
|
||||
blogApp.use(serveSharedFile('sitemap.xsl', 'text/xsl', utils.ONE_DAY_S));
|
||||
|
||||
// Static assets
|
||||
blogApp.use('/shared', express['static'](path.join(corePath, '/shared'), {maxAge: utils.ONE_HOUR_MS}));
|
||||
|
@ -274,6 +273,13 @@ setupMiddleware = function (blogAppInstance, adminApp) {
|
|||
// Theme only config
|
||||
blogApp.use(middleware.staticTheme());
|
||||
|
||||
// Check if password protected blog
|
||||
blogApp.use(middleware.checkIsPrivate); // check if the blog is protected
|
||||
blogApp.use(middleware.filterPrivateRoutes);
|
||||
|
||||
// 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));
|
||||
|
||||
|
@ -315,7 +321,7 @@ setupMiddleware = function (blogAppInstance, adminApp) {
|
|||
blogApp.use('/ghost', adminApp);
|
||||
|
||||
// Set up Frontend routes
|
||||
blogApp.use(routes.frontend());
|
||||
blogApp.use(routes.frontend(middleware));
|
||||
|
||||
// ### Error handling
|
||||
// 404 Handler
|
||||
|
|
|
@ -3,13 +3,17 @@
|
|||
// middleware_spec.js
|
||||
|
||||
var _ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
express = require('express'),
|
||||
bcrypt = require('bcryptjs'),
|
||||
busboy = require('./ghost-busboy'),
|
||||
config = require('../config'),
|
||||
path = require('path'),
|
||||
api = require('../api'),
|
||||
passport = require('passport'),
|
||||
Promise = require('bluebird'),
|
||||
errors = require('../errors'),
|
||||
session = require('cookie-session'),
|
||||
url = require('url'),
|
||||
utils = require('../utils'),
|
||||
|
||||
|
@ -17,7 +21,8 @@ var _ = require('lodash'),
|
|||
blogApp,
|
||||
oauthServer,
|
||||
loginSecurity = [],
|
||||
forgottenSecurity = [];
|
||||
forgottenSecurity = [],
|
||||
protectedSecurity = [];
|
||||
|
||||
function isBlackListedFileType(file) {
|
||||
var blackListedFileTypes = ['.hbs', '.md', '.json'],
|
||||
|
@ -76,6 +81,17 @@ function sslForbiddenOrRedirect(opt) {
|
|||
return response;
|
||||
}
|
||||
|
||||
function verifySessionHash(hash) {
|
||||
if (!hash) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
var bcryptCompare = Promise.promisify(bcrypt.compare);
|
||||
return api.settings.read({context: {internal: true}, key: 'password'}).then(function (response) {
|
||||
var pass = response.settings[0].value;
|
||||
return bcryptCompare(pass, hash);
|
||||
});
|
||||
}
|
||||
|
||||
middleware = {
|
||||
|
||||
// ### Authenticate Middleware
|
||||
|
@ -325,6 +341,132 @@ middleware = {
|
|||
next();
|
||||
},
|
||||
|
||||
checkIsPrivate: function (req, res, next) {
|
||||
return api.settings.read({context: {internal: true}, key: 'isPrivate'}).then(function (response) {
|
||||
var pass = response.settings[0];
|
||||
if (_.isEmpty(pass.value) || pass.value === 'false') {
|
||||
res.isPrivateBlog = false;
|
||||
return next();
|
||||
}
|
||||
res.isPrivateBlog = true;
|
||||
return session({
|
||||
maxAge: utils.ONE_MONTH_MS,
|
||||
keys: ['isPrivateBlog']
|
||||
})(req, res, next);
|
||||
});
|
||||
},
|
||||
|
||||
filterPrivateRoutes: function (req, res, next) {
|
||||
if (res.isAdmin || !res.isPrivateBlog || req.url.lastIndexOf('/private/', 0) === 0) {
|
||||
return next();
|
||||
}
|
||||
if (req.url.lastIndexOf('/rss', 0) === 0 || req.url.lastIndexOf('/sitemap', 0) === 0) { // take care of rss and sitemap 404's
|
||||
return errors.error404(req, res, next);
|
||||
} else if (req.url.lastIndexOf('/robots.txt', 0) === 0) {
|
||||
fs.readFile(path.join(config.paths.corePath, 'shared', 'private-robots.txt'), function (err, buf) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': buf.length,
|
||||
'Cache-Control': 'public, max-age=' + utils.ONE_HOUR_MS
|
||||
});
|
||||
res.end(buf);
|
||||
});
|
||||
} else {
|
||||
return middleware.authenticatePrivateSession(req, res, next);
|
||||
}
|
||||
},
|
||||
|
||||
authenticatePrivateSession: function (req, res, next) {
|
||||
var clientHash = req.session.token || '';
|
||||
return verifySessionHash(clientHash).then(function (isVerified) {
|
||||
if (isVerified) {
|
||||
return next();
|
||||
} else {
|
||||
return res.redirect(config.urlFor({relativeUrl: '/private/'}) + '?r=' + encodeURI(req.url));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// This is here so a call to /private/ after a session is verified will redirect to home;
|
||||
isPrivateSessionAuth: function (req, res, next) {
|
||||
if (!res.isPrivateBlog) {
|
||||
return res.redirect(config.urlFor('home', true));
|
||||
}
|
||||
var hash = req.session.token || '';
|
||||
return verifySessionHash(hash).then(function (isVerified) {
|
||||
if (isVerified) {
|
||||
return res.redirect(config.urlFor('home', true)); // redirect to home if user is already authenticated
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
spamProtectedPrevention: function (req, res, next) {
|
||||
var currentTime = process.hrtime()[0],
|
||||
remoteAddress = req.connection.remoteAddress,
|
||||
rateProtectedPeriod = config.rateProtectedPeriod || 3600,
|
||||
rateProtectedAttempts = config.rateProtectedAttempts || 10,
|
||||
ipCount = '',
|
||||
message = 'Too many attempts.',
|
||||
deniedRateLimit = '',
|
||||
password = req.body.password;
|
||||
|
||||
if (password) {
|
||||
protectedSecurity.push({ip: remoteAddress, time: currentTime});
|
||||
} else {
|
||||
res.error = {
|
||||
message: 'No password entered'
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
// filter entries that are older than rateProtectedPeriod
|
||||
protectedSecurity = _.filter(protectedSecurity, function (logTime) {
|
||||
return (logTime.time + rateProtectedPeriod > currentTime);
|
||||
});
|
||||
|
||||
ipCount = _.chain(protectedSecurity).countBy('ip').value();
|
||||
deniedRateLimit = (ipCount[remoteAddress] > rateProtectedAttempts);
|
||||
|
||||
if (deniedRateLimit) {
|
||||
errors.logError(
|
||||
'Only ' + rateProtectedAttempts + ' tries per IP address every ' + rateProtectedPeriod + ' seconds.',
|
||||
'Too many login attempts.'
|
||||
);
|
||||
message += rateProtectedPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
|
||||
res.error = {
|
||||
message: message
|
||||
};
|
||||
}
|
||||
return next();
|
||||
},
|
||||
|
||||
authenticateProtection: function (req, res, next) {
|
||||
if (res.error) { // if errors have been generated from the previous call
|
||||
return next();
|
||||
}
|
||||
var bodyPass = req.body.password,
|
||||
bcryptHash = Promise.promisify(bcrypt.hash);
|
||||
return api.settings.read({context: {internal: true}, key: 'password'}).then(function (response) {
|
||||
var pass = response.settings[0];
|
||||
if (pass.value === bodyPass) {
|
||||
return bcryptHash(pass.value, 10).then(function (hash) {
|
||||
req.session.token = hash;
|
||||
return res.redirect(config.urlFor({relativeUrl: decodeURI(req.body.forward)}));
|
||||
});
|
||||
} else {
|
||||
res.error = {
|
||||
message: 'Wrong password'
|
||||
};
|
||||
return next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
busboy: busboy
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ var frontend = require('../controllers/frontend'),
|
|||
|
||||
frontendRoutes;
|
||||
|
||||
frontendRoutes = function () {
|
||||
frontendRoutes = function (middleware) {
|
||||
var router = express.Router(),
|
||||
subdir = config.paths.subdir,
|
||||
routeKeywords = config.routeKeywords;
|
||||
|
@ -28,6 +28,18 @@ frontendRoutes = function () {
|
|||
res.redirect(subdir + '/ghost/');
|
||||
});
|
||||
|
||||
// password-protected frontend route
|
||||
router.get('/private/',
|
||||
middleware.isPrivateSessionAuth,
|
||||
frontend.private
|
||||
);
|
||||
router.post('/private/',
|
||||
middleware.isPrivateSessionAuth,
|
||||
middleware.spamProtectedPrevention,
|
||||
middleware.authenticateProtection,
|
||||
frontend.private
|
||||
);
|
||||
|
||||
// ### Frontend routes
|
||||
router.get('/rss/', frontend.rss);
|
||||
router.get('/rss/:page/', frontend.rss);
|
||||
|
|
|
@ -24,6 +24,7 @@ utils = {
|
|||
ONE_YEAR_S: 31536000,
|
||||
ONE_HOUR_MS: 3600000,
|
||||
ONE_DAY_MS: 86400000,
|
||||
ONE_MONTH_MS: 2628000000,
|
||||
ONE_YEAR_MS: 31536000000,
|
||||
|
||||
/**
|
||||
|
|
56
core/server/views/password.hbs
Normal file
56
core/server/views/password.hbs
Normal file
|
@ -0,0 +1,56 @@
|
|||
<!doctype html>
|
||||
<!--[if (IE 8)&!(IEMobile)]><html class="no-js lt-ie9" lang="en"><![endif]-->
|
||||
<!--[if (gte IE 9)| IEMobile |!(IE)]><!--><html class="no-js" lang="en"><!--<![endif]-->
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
|
||||
<title>Ghost - Private Blog Access</title>
|
||||
|
||||
<meta name="description" content="{{siteDescription}}">
|
||||
<meta name="author" content="">
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
<meta name="MobileOptimized" content="320">
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<link rel="shortcut icon" href="{{asset "favicon.ico"}}">
|
||||
<meta http-equiv="cleartype" content="on">
|
||||
|
||||
<link rel="stylesheet" type='text/css' href='//fonts.googleapis.com/css?family=Open+Sans:400,300,700'>
|
||||
<link rel="stylesheet" href="{{asset "ghost.css" ghost="true" minifyInProduction="true"}}" />
|
||||
</head>
|
||||
<body class="ghost-setup">
|
||||
<main role="main" id="main" class="viewport">
|
||||
<section class="setup-box js-setup-box fade-in">
|
||||
<div class="vertical">
|
||||
<form id="setup" class="setup-form" method="post" novalidate="novalidate">
|
||||
<header>
|
||||
<h1>This blog is private</h1>
|
||||
</header>
|
||||
<input type="hidden" name="forward" value="{{forward}}">
|
||||
<div class="form-group">
|
||||
<input type="text" name="password" autofocus="autofocus" class="icon-lock">
|
||||
</div>
|
||||
<footer>
|
||||
<button type="submit" class="btn btn-green btn-lg">
|
||||
Enter
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{{#if error}}
|
||||
<aside class="notification bottom">
|
||||
<div class="js-bb-notification">
|
||||
<section class="js-notification notification-error">
|
||||
<span class="notification-message">{{error.message}}</span>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
{{/if}}
|
||||
<script src="{{asset "ghost.js" ghost="true" minifyInProduction="true"}}"></script>
|
||||
</body>
|
||||
</html>
|
2
core/shared/private-robots.txt
Normal file
2
core/shared/private-robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
|
@ -1438,4 +1438,79 @@ describe('Frontend Controller', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('private', function () {
|
||||
var req, res, config, defaultPath;
|
||||
|
||||
defaultPath = '/core/server/views/password.hbs';
|
||||
|
||||
beforeEach(function () {
|
||||
res = {
|
||||
locals: {verson: ''},
|
||||
render: sandbox.spy()
|
||||
},
|
||||
req = {
|
||||
query: {
|
||||
r: ''
|
||||
}
|
||||
},
|
||||
config = {
|
||||
paths: {
|
||||
adminViews: '/core/server/views',
|
||||
availableThemes: {
|
||||
casper: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
apiSettingsStub = sandbox.stub(api.settings, 'read');
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'activeTheme')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'activeTheme',
|
||||
value: 'casper'
|
||||
}]
|
||||
}));
|
||||
});
|
||||
|
||||
it('Should render default password page when theme has no password template', function (done) {
|
||||
frontend.__set__('config', config);
|
||||
|
||||
frontend.private(req, res, done).then(function () {
|
||||
res.render.calledWith(defaultPath).should.be.true;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Should render theme password page when it exists', function (done) {
|
||||
config.paths.availableThemes.casper = {
|
||||
'password.hbs': '/content/themes/casper/password.hbs'
|
||||
};
|
||||
frontend.__set__('config', config);
|
||||
|
||||
frontend.private(req, res, done).then(function () {
|
||||
res.render.calledWith('password').should.be.true;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Should render with forward data when it is passed in', function (done) {
|
||||
frontend.__set__('config', config);
|
||||
req.query.r = '/test-redirect/';
|
||||
|
||||
frontend.private(req, res, done).then(function () {
|
||||
res.render.calledWith(defaultPath, {forward: '/test-redirect/'}).should.be.true;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('Should render with error when error is passed in', function (done) {
|
||||
frontend.__set__('config', config);
|
||||
res.error = 'Test Error';
|
||||
|
||||
frontend.private(req, res, done).then(function () {
|
||||
res.render.calledWith(defaultPath, {forward: '', error: 'Test Error'}).should.be.true;
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,9 +3,24 @@
|
|||
var assert = require('assert'),
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
middleware = require('../../server/middleware').middleware;
|
||||
Promise = require('bluebird'),
|
||||
middleware = require('../../server/middleware').middleware,
|
||||
api = require('../../server/api'),
|
||||
errors = require('../../server/errors'),
|
||||
bcrypt = require('bcryptjs'),
|
||||
fs = require('fs');
|
||||
|
||||
describe('Middleware', function () {
|
||||
var sandbox,
|
||||
apiSettingsStub;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
// TODO: needs new test for ember admin
|
||||
// describe('redirectToDashboard', function () {
|
||||
// var req, res;
|
||||
|
@ -236,4 +251,209 @@ describe('Middleware', function () {
|
|||
response.redirectUrl({a: 'b'}).should.equal('https://example.com/ssl/config/path/req/path?a=b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('passProtect', function () {
|
||||
var req, res, next;
|
||||
|
||||
beforeEach(function () {
|
||||
req = {}, res = {};
|
||||
apiSettingsStub = sandbox.stub(api.settings, 'read');
|
||||
next = sinon.spy();
|
||||
});
|
||||
|
||||
it('checkIsPrivate should call next if not private', function (done) {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'isPrivate',
|
||||
value: 'false'
|
||||
}]
|
||||
}));
|
||||
|
||||
middleware.checkIsPrivate(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
res.isPrivateBlog.should.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('checkIsPrivate should load session if private', function (done) {
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'isPrivate')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'isPrivate',
|
||||
value: 'true'
|
||||
}]
|
||||
}));
|
||||
|
||||
middleware.checkIsPrivate(req, res, next).then(function () {
|
||||
res.isPrivateBlog.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('not private', function () {
|
||||
beforeEach(function () {
|
||||
res.isPrivateBlog = false;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if not private', function () {
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should redirect if blog is not private', function () {
|
||||
res = {
|
||||
redirect: sinon.spy(),
|
||||
isPrivateBlog: false
|
||||
};
|
||||
middleware.isPrivateSessionAuth(req, res, next);
|
||||
res.redirect.called.should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('private', function () {
|
||||
var errorSpy;
|
||||
|
||||
beforeEach(function () {
|
||||
res.isPrivateBlog = true;
|
||||
errorSpy = sandbox.spy(errors, 'error404');
|
||||
res = {
|
||||
status: function () {
|
||||
return this;
|
||||
},
|
||||
send: function () {},
|
||||
set: function () {},
|
||||
isPrivateBlog: true
|
||||
};
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if admin', function () {
|
||||
res.isAdmin = true;
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should call next if is the "private" route', function () {
|
||||
req.url = '/private/';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is sitemap', function () {
|
||||
req.url = '/sitemap.xml';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is rss', function () {
|
||||
req.url = '/rss';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should throw 404 if url is rss plus something', function () {
|
||||
req.url = '/rss/sometag';
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
errorSpy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('filterPrivateRoutes should render custom robots.txt', function () {
|
||||
req.url = '/robots.txt';
|
||||
res.writeHead = sinon.spy();
|
||||
res.end = sinon.spy();
|
||||
sandbox.stub(fs, 'readFile', function (file, cb) {
|
||||
cb(null, 'User-agent: * Disallow: /');
|
||||
});
|
||||
middleware.filterPrivateRoutes(req, res, next);
|
||||
res.writeHead.called.should.be.true;
|
||||
res.end.called.should.be.true;
|
||||
});
|
||||
|
||||
it('authenticateProtection should call next if error', function () {
|
||||
res.error = 'Test Error';
|
||||
middleware.authenticateProtection(req, res, next);
|
||||
next.called.should.be.true;
|
||||
});
|
||||
|
||||
describe('with hash verification', function () {
|
||||
beforeEach(function () {
|
||||
sandbox.stub(bcrypt, 'compare', function (check, hash, cb) {
|
||||
var isVerified = (check === hash) ? true : false;
|
||||
cb(null, isVerified);
|
||||
});
|
||||
apiSettingsStub.withArgs(sinon.match.has('key', 'password')).returns(Promise.resolve({
|
||||
settings: [{
|
||||
key: 'password',
|
||||
value: 'rightpassword'
|
||||
}]
|
||||
}));
|
||||
});
|
||||
|
||||
it('authenticatePrivateSession should return next if hash is verified', function (done) {
|
||||
req.session = {
|
||||
token: 'rightpassword'
|
||||
};
|
||||
middleware.authenticatePrivateSession(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('authenticatePrivateSession should redirect if hash is not verified', function (done) {
|
||||
req.url = '/welcome-to-ghost';
|
||||
req.session = {
|
||||
token: 'wrongpassword'
|
||||
};
|
||||
res.redirect = sinon.spy();
|
||||
middleware.authenticatePrivateSession(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should redirect if hash is verified', function (done) {
|
||||
req.session = {
|
||||
token: 'rightpassword'
|
||||
};
|
||||
res.redirect = sandbox.spy();
|
||||
middleware.isPrivateSessionAuth(req, res, next).then(function () {
|
||||
res.redirect.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('isPrivateSessionAuth should return next if hash is not verified', function (done) {
|
||||
req.session = {
|
||||
token: 'wrongpassword'
|
||||
};
|
||||
middleware.isPrivateSessionAuth(req, res, next).then(function () {
|
||||
next.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('authenticateProtection should return next if password is incorrect', function (done) {
|
||||
req.body = {password:'wrongpassword'};
|
||||
middleware.authenticateProtection(req, res, next).then(function () {
|
||||
res.error.should.not.be.empty;
|
||||
next.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('authenticateProtection should redirect if password is correct', function (done) {
|
||||
req.body = {password:'rightpassword'};
|
||||
req.session = {};
|
||||
res.redirect = sandbox.spy();
|
||||
sandbox.stub(bcrypt, 'hash', function (pass, salt, cb) {
|
||||
cb(null, pass + 'hash');
|
||||
});
|
||||
middleware.authenticateProtection(req, res, next).then(function () {
|
||||
req.session.token.should.equal('rightpasswordhash');
|
||||
res.redirect.called.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"iojs": "~1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "2.0.2",
|
||||
"bcryptjs": "2.1.0",
|
||||
"bluebird": "2.4.2",
|
||||
"body-parser": "1.10.0",
|
||||
"bookshelf": "0.7.9",
|
||||
|
@ -40,6 +40,7 @@
|
|||
"colors": "0.6.2",
|
||||
"compression": "1.2.2",
|
||||
"connect-slashes": "1.3.0",
|
||||
"cookie-session": "^1.1.0",
|
||||
"downsize": "0.0.8",
|
||||
"express": "4.12.0",
|
||||
"express-hbs": "0.8.4",
|
||||
|
|
Loading…
Reference in a new issue